react-email 4.0.0-alpha.5 → 4.0.0-alpha.6
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 +6 -0
- package/dist/cli/index.js +1175 -2659
- package/dist/cli/index.mjs +16 -14
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +31 -31
- package/dist/preview/.next/app-path-routes-manifest.json +6 -1
- package/dist/preview/.next/build-manifest.json +14 -14
- 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/diagnostics/framework.json +1 -1
- package/dist/preview/.next/export-marker.json +6 -1
- package/dist/preview/.next/images-manifest.json +57 -1
- 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 +41 -1
- package/dist/preview/.next/required-server-files.json +310 -1
- package/dist/preview/.next/routes-manifest.json +64 -1
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page.js.nft.json +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/favicon.ico/route.js.nft.json +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 +47 -11
- 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/171.js +14 -0
- package/dist/preview/.next/server/chunks/446.js +6 -0
- package/dist/preview/.next/server/chunks/600.js +8 -0
- package/dist/preview/.next/server/chunks/811.js +13 -0
- package/dist/preview/.next/server/chunks/833.js +1 -0
- package/dist/preview/.next/server/functions-config-manifest.json +4 -1
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/pages/_app.js +1 -1
- package/dist/preview/.next/server/pages/_app.js.nft.json +1 -1
- package/dist/preview/.next/server/pages/_document.js +1 -1
- package/dist/preview/.next/server/pages/_document.js.nft.json +1 -1
- package/dist/preview/.next/server/pages/_error.js +1 -1
- package/dist/preview/.next/server/pages/_error.js.nft.json +1 -1
- package/dist/preview/.next/server/pages-manifest.json +5 -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/server/webpack-runtime.js +1 -1
- package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +1 -0
- package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +1 -0
- package/dist/preview/.next/static/chunks/744-79730358b37b2212.js +1 -0
- package/dist/preview/.next/static/chunks/781-5f16c6bc9d9d4cc1.js +1 -0
- package/dist/preview/.next/static/chunks/832ad4be-cb988facfb8f955f.js +1 -0
- package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +1 -0
- package/dist/preview/.next/static/chunks/{afa401a5-a600c227dacf3ab4.js → afa401a5-3e949a1cfd317dd3.js} +3 -3
- package/dist/preview/.next/static/chunks/app/_not-found/page-09d694081cc9d4dc.js +1 -0
- package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +1 -0
- package/dist/preview/.next/static/chunks/framework-c2bd6d936e3077bc.js +1 -0
- package/dist/preview/.next/static/chunks/main-44463a8301435b64.js +1 -0
- package/dist/preview/.next/static/chunks/main-app-c2e686acf8d370d7.js +1 -0
- package/dist/preview/.next/static/chunks/pages/_app-f3011d3f00bb8dba.js +1 -0
- package/dist/preview/.next/static/chunks/pages/_error-39a87dee2e97a2a3.js +1 -0
- package/dist/preview/.next/static/chunks/{webpack-2eb145a20ee6cb77.js → webpack-41e2667c9f086a4f.js} +1 -1
- package/dist/preview/.next/static/css/d7df9cfc3e182163.css +3 -0
- package/dist/preview/.next/static/gFk9UfWL8joM4iD7-wlKF/_buildManifest.js +1 -0
- package/dist/preview/.next/trace +26 -22
- package/dist/preview/.next/types/cache-life.d.ts +3 -3
- package/package.json +14 -9
- package/scripts/build-preview-server.mjs +32 -0
- package/scripts/fill-caniemail-data.mjs +36 -0
- package/src/actions/email-validation/caniemail-data.ts +85993 -0
- package/src/actions/email-validation/check-compatibility.ts +322 -0
- package/src/actions/email-validation/check-images.spec.tsx +2 -2
- package/src/actions/email-validation/check-images.ts +2 -2
- package/src/actions/email-validation/check-links.spec.tsx +4 -4
- package/src/actions/email-validation/check-links.ts +2 -2
- package/src/actions/get-email-path-from-slug.ts +1 -1
- package/src/actions/render-email-by-path.tsx +2 -1
- package/src/{utils/emails-directory-absolute-path.ts → app/env.ts} +2 -0
- package/src/app/layout.tsx +1 -1
- package/src/app/page.tsx +1 -1
- package/src/app/preview/[...slug]/page.tsx +73 -16
- package/src/app/preview/[...slug]/preview.tsx +11 -57
- package/src/components/code.tsx +0 -1
- package/src/components/toolbar/linter.tsx +267 -124
- package/src/components/toolbar/spam-assassin.tsx +20 -31
- package/src/components/toolbar/toolbar-button.tsx +50 -0
- package/src/components/toolbar/use-cached-state.ts +33 -0
- package/src/components/toolbar.tsx +106 -98
- package/src/components/topbar/view-size-controls.tsx +1 -0
- package/src/components/topbar.tsx +3 -9
- package/src/contexts/emails.tsx +2 -1
- package/src/contexts/preview.tsx +81 -0
- package/src/hooks/use-email-rendering-result.ts +2 -1
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
- package/src/utils/caniemail/all-css-properties.ts +358 -0
- package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
- package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
- package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
- package/src/utils/caniemail/get-css-functions.ts +25 -0
- package/src/utils/caniemail/get-css-property-names.ts +32 -0
- package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
- package/src/utils/caniemail/get-css-unit.ts +3 -0
- package/src/utils/caniemail/get-element-attributes.ts +7 -0
- package/src/utils/caniemail/get-element-names.ts +20 -0
- package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +205 -0
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
- package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
- package/src/utils/get-email-component.ts +34 -67
- package/src/utils/linting.ts +85 -0
- package/src/utils/result.ts +49 -0
- package/src/utils/run-bundled-code.ts +64 -0
- package/tailwind-internals.d.ts +133 -0
- package/tsconfig.json +9 -3
- package/build-preview-server.mjs +0 -25
- package/dist/preview/.next/cache/images/TcyzHbFXGFjrOu3wEMvDoSmqCh3qP3iiNqJf0QbED9Y/60.1741728556140.cQ5qicbpvoXZ7leVmWqG2ElLwXB1ynYeSv8MBSA-QeM.Vy8iMWM3MGUtMTk1ODcxYmIyNzMi.webp +0 -0
- package/dist/preview/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/1.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/10.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/11.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/12.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/13.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/2.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/3.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/4.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/5.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/6.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/7.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/8.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/9.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/index.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
- package/dist/preview/.next/cache/webpack/server-development/0.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/1.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/2.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/3.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/4.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/5.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/6.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/7.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/8.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/9.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/index.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
- package/dist/preview/.next/server/chunks/143.js +0 -6
- package/dist/preview/.next/server/chunks/409.js +0 -5
- package/dist/preview/.next/server/chunks/46.js +0 -1
- package/dist/preview/.next/server/chunks/478.js +0 -14
- package/dist/preview/.next/server/chunks/707.js +0 -13
- package/dist/preview/.next/static/B4EYZiVzdylEG9lAIl-aO/_buildManifest.js +0 -1
- package/dist/preview/.next/static/chunks/575-bc52750855c25df4.js +0 -2
- package/dist/preview/.next/static/chunks/684-0f1ef7361c499798.js +0 -1
- package/dist/preview/.next/static/chunks/684c6b30-0c65da32762fc4ee.js +0 -1
- package/dist/preview/.next/static/chunks/81-e7539b08d9d3fb4d.js +0 -1
- package/dist/preview/.next/static/chunks/883-70c8267c50bc4133.js +0 -1
- package/dist/preview/.next/static/chunks/921-d1dc8c63f49e85d6.js +0 -1
- package/dist/preview/.next/static/chunks/app/_not-found/page-03ce767859c36d4e.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-7cf14e28880544f1.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-065cb49b0a078541.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-656510fd180c803c.js +0 -1
- package/dist/preview/.next/static/chunks/framework-2a724981073c3a29.js +0 -1
- package/dist/preview/.next/static/chunks/main-552b9719bbc3a274.js +0 -1
- package/dist/preview/.next/static/chunks/main-app-914a73336fd45af5.js +0 -1
- package/dist/preview/.next/static/chunks/pages/_app-77ca34bce25ac75c.js +0 -1
- package/dist/preview/.next/static/chunks/pages/_error-73f611c46abbb495.js +0 -1
- package/dist/preview/.next/static/css/2df96d9ee014e8de.css +0 -3
- /package/dist/preview/.next/static/{B4EYZiVzdylEG9lAIl-aO → gFk9UfWL8joM4iD7-wlKF}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { motion } from 'framer-motion';
|
|
2
|
+
import { cn } from '../../utils';
|
|
3
|
+
import { Tooltip } from '../tooltip';
|
|
4
|
+
|
|
5
|
+
interface ToolbarButtonProps extends React.ComponentProps<'button'> {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
tooltip?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ToolbarButton = ({
|
|
12
|
+
children,
|
|
13
|
+
className,
|
|
14
|
+
active,
|
|
15
|
+
tooltip,
|
|
16
|
+
...props
|
|
17
|
+
}: ToolbarButtonProps) => {
|
|
18
|
+
return (
|
|
19
|
+
<Tooltip.Provider>
|
|
20
|
+
<Tooltip>
|
|
21
|
+
<Tooltip.Trigger asChild>
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
{...props}
|
|
25
|
+
className={cn(
|
|
26
|
+
'h-full w-fit font-medium flex text-sm text-slate-10 items-center align-middle justify-center px-1 py-2 gap-1 relative',
|
|
27
|
+
'hover:text-slate-12 transition-colors',
|
|
28
|
+
active && 'data-[state=active]:text-cyan-11',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
{active ? (
|
|
34
|
+
<motion.span
|
|
35
|
+
className="-bottom-px absolute rounded-sm left-0 w-full bg-cyan-11 h-px"
|
|
36
|
+
layoutId="active-toolbar-button"
|
|
37
|
+
transition={{
|
|
38
|
+
type: 'spring',
|
|
39
|
+
bounce: 0.2,
|
|
40
|
+
duration: 0.6,
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
) : null}
|
|
44
|
+
</button>
|
|
45
|
+
</Tooltip.Trigger>
|
|
46
|
+
{tooltip ? <Tooltip.Content>{tooltip}</Tooltip.Content> : null}
|
|
47
|
+
</Tooltip>
|
|
48
|
+
</Tooltip.Provider>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useCachedState = <T>(key: string) => {
|
|
4
|
+
let value: T | undefined = undefined;
|
|
5
|
+
if ('localStorage' in global) {
|
|
6
|
+
const storedValue = global.localStorage.getItem(key);
|
|
7
|
+
if (storedValue !== null) {
|
|
8
|
+
try {
|
|
9
|
+
value = JSON.parse(storedValue) as T;
|
|
10
|
+
} catch (exception) {
|
|
11
|
+
console.warn(
|
|
12
|
+
'Failed to load stored value for',
|
|
13
|
+
key,
|
|
14
|
+
'with value',
|
|
15
|
+
value,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
useSyncExternalStore(
|
|
23
|
+
() => () => {},
|
|
24
|
+
() => value,
|
|
25
|
+
() => undefined,
|
|
26
|
+
),
|
|
27
|
+
function setValue(newValue: T | undefined) {
|
|
28
|
+
if ('localStorage' in global) {
|
|
29
|
+
global.localStorage.setItem(key, JSON.stringify(newValue));
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
] as const;
|
|
33
|
+
};
|
|
@@ -1,90 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import * as Tabs from '@radix-ui/react-tabs';
|
|
2
|
-
import { LayoutGroup
|
|
3
|
+
import { LayoutGroup } from 'framer-motion';
|
|
3
4
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
4
|
-
import { useEffect } from 'react';
|
|
5
|
+
import { use, useEffect } from 'react';
|
|
6
|
+
import { isBuilding } from '../app/env';
|
|
7
|
+
import { PreviewContext } from '../contexts/preview';
|
|
5
8
|
import { cn } from '../utils';
|
|
6
9
|
import { IconArrowDown } from './icons/icon-arrow-down';
|
|
7
10
|
import { IconReload } from './icons/icon-reload';
|
|
8
11
|
import { IconScanner } from './icons/icon-scanner';
|
|
9
12
|
import { IconScissors } from './icons/icon-scissors';
|
|
10
|
-
import { Linter, useLinter } from './toolbar/linter';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
+
import { Linter, type LintingRow, useLinter } from './toolbar/linter';
|
|
14
|
+
import {
|
|
15
|
+
SpamAssassin,
|
|
16
|
+
type SpamCheckingResult,
|
|
17
|
+
useSpamAssassin,
|
|
18
|
+
} from './toolbar/spam-assassin';
|
|
19
|
+
import { ToolbarButton } from './toolbar/toolbar-button';
|
|
20
|
+
import { useCachedState } from './toolbar/use-cached-state';
|
|
13
21
|
|
|
14
|
-
type
|
|
15
|
-
emailSlug: string;
|
|
16
|
-
markup: string;
|
|
17
|
-
plainText: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type ActivePanelValue = 'linter' | 'spam-assassin';
|
|
22
|
+
export type ToolbarTabValue = 'linter' | 'spam-assassin';
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
tooltip?: React.ReactNode;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ToolbarButton = ({
|
|
29
|
-
children,
|
|
30
|
-
className,
|
|
31
|
-
active,
|
|
32
|
-
tooltip,
|
|
33
|
-
...props
|
|
34
|
-
}: ToolbarButton) => {
|
|
35
|
-
return (
|
|
36
|
-
<Tooltip.Provider>
|
|
37
|
-
<Tooltip>
|
|
38
|
-
<Tooltip.Trigger asChild>
|
|
39
|
-
<button
|
|
40
|
-
type="button"
|
|
41
|
-
{...props}
|
|
42
|
-
className={cn(
|
|
43
|
-
'h-full w-fit font-medium flex text-sm text-slate-10 items-center align-middle justify-center px-1 py-2 gap-1 relative',
|
|
44
|
-
'hover:text-slate-12 transition-colors',
|
|
45
|
-
active && 'data-[state=active]:text-cyan-11',
|
|
46
|
-
className,
|
|
47
|
-
)}
|
|
48
|
-
>
|
|
49
|
-
{children}
|
|
50
|
-
{active ? (
|
|
51
|
-
<motion.span
|
|
52
|
-
className="-bottom-px absolute rounded-sm left-0 w-full bg-cyan-11 h-px"
|
|
53
|
-
layoutId="active-toolbar-button"
|
|
54
|
-
transition={{
|
|
55
|
-
type: 'spring',
|
|
56
|
-
bounce: 0.2,
|
|
57
|
-
duration: 0.6,
|
|
58
|
-
}}
|
|
59
|
-
/>
|
|
60
|
-
) : null}
|
|
61
|
-
</button>
|
|
62
|
-
</Tooltip.Trigger>
|
|
63
|
-
{tooltip ? <Tooltip.Content>{tooltip}</Tooltip.Content> : null}
|
|
64
|
-
</Tooltip>
|
|
65
|
-
</Tooltip.Provider>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
24
|
+
const ToolbarInner = ({
|
|
25
|
+
serverLintingRows,
|
|
26
|
+
serverSpamCheckingResult,
|
|
68
27
|
|
|
69
|
-
export const Toolbar = ({
|
|
70
|
-
emailSlug,
|
|
71
28
|
markup,
|
|
29
|
+
reactMarkup,
|
|
72
30
|
plainText,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}: ToolbarProps
|
|
31
|
+
emailPath,
|
|
32
|
+
emailSlug,
|
|
33
|
+
}: ToolbarProps & {
|
|
34
|
+
markup: string;
|
|
35
|
+
reactMarkup: string;
|
|
36
|
+
plainText: string;
|
|
37
|
+
emailSlug: string;
|
|
38
|
+
emailPath: string;
|
|
39
|
+
}) => {
|
|
76
40
|
const pathname = usePathname();
|
|
77
41
|
const searchParams = useSearchParams();
|
|
78
42
|
const router = useRouter();
|
|
79
43
|
|
|
80
|
-
const
|
|
81
|
-
|
|
|
44
|
+
const activeTab = (searchParams.get('toolbar-panel') ?? undefined) as
|
|
45
|
+
| ToolbarTabValue
|
|
82
46
|
| undefined;
|
|
83
47
|
|
|
84
|
-
const toggled =
|
|
48
|
+
const toggled = activeTab !== undefined;
|
|
85
49
|
|
|
86
|
-
const setActivePanelValue = (newValue:
|
|
87
|
-
console.log(newValue);
|
|
50
|
+
const setActivePanelValue = (newValue: ToolbarTabValue | undefined) => {
|
|
88
51
|
const params = new URLSearchParams(searchParams);
|
|
89
52
|
if (newValue === undefined) {
|
|
90
53
|
params.delete('toolbar-panel');
|
|
@@ -94,36 +57,52 @@ export const Toolbar = ({
|
|
|
94
57
|
router.push(`${pathname}?${params.toString()}`);
|
|
95
58
|
};
|
|
96
59
|
|
|
60
|
+
const [cachedSpamCheckingResult, setCachedSpamCheckingResult] =
|
|
61
|
+
useCachedState<SpamCheckingResult>(
|
|
62
|
+
`spam-assassin-${emailSlug.replaceAll('/', '-')}`,
|
|
63
|
+
);
|
|
97
64
|
const [spamCheckingResult, { load: loadSpamChecking }] = useSpamAssassin({
|
|
98
|
-
slug: emailSlug,
|
|
99
65
|
markup,
|
|
100
66
|
plainText,
|
|
67
|
+
|
|
68
|
+
initialResult: serverSpamCheckingResult ?? cachedSpamCheckingResult,
|
|
101
69
|
});
|
|
102
70
|
|
|
103
|
-
const [
|
|
104
|
-
|
|
71
|
+
const [cachedLintingRows, setCachedLintingRows] = useCachedState<
|
|
72
|
+
LintingRow[]
|
|
73
|
+
>(`linter-${emailSlug.replaceAll('/', '-')}`);
|
|
74
|
+
const [lintingRows, { load: loadLinting }] = useLinter({
|
|
75
|
+
reactMarkup,
|
|
76
|
+
emailPath,
|
|
105
77
|
markup,
|
|
78
|
+
|
|
79
|
+
initialRows: serverLintingRows ?? cachedLintingRows,
|
|
106
80
|
});
|
|
107
81
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
82
|
+
if (!isBuilding) {
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
(async () => {
|
|
85
|
+
const lintingRows = await loadLinting();
|
|
86
|
+
setCachedLintingRows(lintingRows);
|
|
87
|
+
|
|
88
|
+
const spamCheckingResult = await loadSpamChecking();
|
|
89
|
+
setCachedSpamCheckingResult(spamCheckingResult);
|
|
90
|
+
})();
|
|
91
|
+
}, []);
|
|
92
|
+
}
|
|
112
93
|
|
|
113
94
|
return (
|
|
114
95
|
<div
|
|
115
|
-
{...rest}
|
|
116
96
|
data-toggled={toggled}
|
|
117
97
|
className={cn(
|
|
118
98
|
'bg-black group/toolbar text-xs text-slate-11 h-48 transition-all',
|
|
119
99
|
'data-[toggled=false]:h-8',
|
|
120
|
-
className,
|
|
121
100
|
)}
|
|
122
101
|
>
|
|
123
102
|
<Tabs.Root
|
|
124
|
-
value={
|
|
103
|
+
value={activeTab}
|
|
125
104
|
onValueChange={(newValue) => {
|
|
126
|
-
setActivePanelValue(newValue as
|
|
105
|
+
setActivePanelValue(newValue as ToolbarTabValue);
|
|
127
106
|
}}
|
|
128
107
|
asChild
|
|
129
108
|
>
|
|
@@ -131,38 +110,40 @@ export const Toolbar = ({
|
|
|
131
110
|
<Tabs.List className="flex gap-4 px-2 border-b border-solid border-slate-6 h-7 w-full">
|
|
132
111
|
<LayoutGroup id="toolbar">
|
|
133
112
|
<Tabs.Trigger asChild value="spam-assassin">
|
|
134
|
-
<ToolbarButton active={
|
|
113
|
+
<ToolbarButton active={activeTab === 'spam-assassin'}>
|
|
135
114
|
<IconScissors />
|
|
136
115
|
Spam Assassin
|
|
137
116
|
</ToolbarButton>
|
|
138
117
|
</Tabs.Trigger>
|
|
139
118
|
<Tabs.Trigger asChild value="linter">
|
|
140
|
-
<ToolbarButton active={
|
|
119
|
+
<ToolbarButton active={activeTab === 'linter'}>
|
|
141
120
|
<IconScanner />
|
|
142
121
|
Linter
|
|
143
122
|
</ToolbarButton>
|
|
144
123
|
</Tabs.Trigger>
|
|
145
124
|
</LayoutGroup>
|
|
146
125
|
<div className="flex gap-1 ml-auto">
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
126
|
+
{isBuilding ? null : (
|
|
127
|
+
<ToolbarButton
|
|
128
|
+
tooltip="Reload"
|
|
129
|
+
onClick={async () => {
|
|
130
|
+
if (activeTab === undefined) {
|
|
131
|
+
setActivePanelValue('linter');
|
|
132
|
+
}
|
|
133
|
+
if (activeTab === 'spam-assassin') {
|
|
134
|
+
await loadSpamChecking();
|
|
135
|
+
} else {
|
|
136
|
+
await loadLinting();
|
|
137
|
+
}
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<IconReload />
|
|
141
|
+
</ToolbarButton>
|
|
142
|
+
)}
|
|
162
143
|
<ToolbarButton
|
|
163
144
|
tooltip="Toggle toolbar"
|
|
164
145
|
onClick={() => {
|
|
165
|
-
if (
|
|
146
|
+
if (activeTab === undefined) {
|
|
166
147
|
setActivePanelValue('linter');
|
|
167
148
|
} else {
|
|
168
149
|
setActivePanelValue(undefined);
|
|
@@ -176,7 +157,7 @@ export const Toolbar = ({
|
|
|
176
157
|
|
|
177
158
|
<div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto px-2">
|
|
178
159
|
<Tabs.Content value="linter">
|
|
179
|
-
<Linter
|
|
160
|
+
<Linter rows={lintingRows} />
|
|
180
161
|
</Tabs.Content>
|
|
181
162
|
<Tabs.Content value="spam-assassin">
|
|
182
163
|
<SpamAssassin result={spamCheckingResult} />
|
|
@@ -187,3 +168,30 @@ export const Toolbar = ({
|
|
|
187
168
|
</div>
|
|
188
169
|
);
|
|
189
170
|
};
|
|
171
|
+
|
|
172
|
+
interface ToolbarProps {
|
|
173
|
+
serverSpamCheckingResult: SpamCheckingResult | undefined;
|
|
174
|
+
serverLintingRows: LintingRow[] | undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const Toolbar = ({
|
|
178
|
+
serverLintingRows,
|
|
179
|
+
serverSpamCheckingResult,
|
|
180
|
+
}: ToolbarProps) => {
|
|
181
|
+
const { emailPath, emailSlug, renderedEmailMetadata } = use(PreviewContext)!;
|
|
182
|
+
|
|
183
|
+
if (renderedEmailMetadata === undefined) return null;
|
|
184
|
+
const { markup, plainText, reactMarkup } = renderedEmailMetadata;
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<ToolbarInner
|
|
188
|
+
emailPath={emailPath}
|
|
189
|
+
emailSlug={emailSlug}
|
|
190
|
+
markup={markup}
|
|
191
|
+
reactMarkup={reactMarkup}
|
|
192
|
+
plainText={plainText}
|
|
193
|
+
serverLintingRows={serverLintingRows}
|
|
194
|
+
serverSpamCheckingResult={serverSpamCheckingResult}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
@@ -7,17 +7,11 @@ import { ShellContext } from './shell';
|
|
|
7
7
|
import { Tooltip } from './tooltip';
|
|
8
8
|
|
|
9
9
|
interface TopbarProps {
|
|
10
|
-
|
|
11
|
-
pathSeparator: string;
|
|
12
|
-
|
|
10
|
+
emailTitle: string;
|
|
13
11
|
children: React.ReactNode;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
export const Topbar = ({
|
|
17
|
-
currentEmailOpenSlug,
|
|
18
|
-
pathSeparator,
|
|
19
|
-
children,
|
|
20
|
-
}: TopbarProps) => {
|
|
14
|
+
export const Topbar = ({ emailTitle, children }: TopbarProps) => {
|
|
21
15
|
const { toggleSidebar } = use(ShellContext)!;
|
|
22
16
|
|
|
23
17
|
return (
|
|
@@ -40,7 +34,7 @@ export const Topbar = ({
|
|
|
40
34
|
</Tooltip>
|
|
41
35
|
<div className="hidden items-center overflow-hidden text-center lg:flex">
|
|
42
36
|
<Heading as="h2" className="truncate" size="2" weight="medium">
|
|
43
|
-
{
|
|
37
|
+
{emailTitle}
|
|
44
38
|
</Heading>
|
|
45
39
|
</div>
|
|
46
40
|
</div>
|
package/src/contexts/emails.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { createContext, useContext, useState } from 'react';
|
|
3
3
|
import { getEmailsDirectoryMetadataAction } from '../actions/get-emails-directory-metadata-action';
|
|
4
|
+
import { isBuilding } from '../app/env';
|
|
4
5
|
import { useHotreload } from '../hooks/use-hot-reload';
|
|
5
6
|
import type { EmailsDirectory } from '../utils/get-emails-directory-metadata';
|
|
6
7
|
|
|
@@ -30,7 +31,7 @@ export const EmailsProvider = (props: {
|
|
|
30
31
|
const [emailsDirectoryMetadata, setEmailsDirectoryMetadata] =
|
|
31
32
|
useState<EmailsDirectory>(props.initialEmailsDirectoryMetadata);
|
|
32
33
|
|
|
33
|
-
if (
|
|
34
|
+
if (!isBuilding) {
|
|
34
35
|
// this will not change on runtime so it doesn't violate
|
|
35
36
|
// the rules of hooks
|
|
36
37
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRouter } from 'next/navigation';
|
|
3
|
+
import { createContext } from 'react';
|
|
4
|
+
import type {
|
|
5
|
+
EmailRenderingResult,
|
|
6
|
+
RenderedEmailMetadata,
|
|
7
|
+
} from '../actions/render-email-by-path';
|
|
8
|
+
import { isBuilding } from '../app/env';
|
|
9
|
+
import { useEmailRenderingResult } from '../hooks/use-email-rendering-result';
|
|
10
|
+
import { useHotreload } from '../hooks/use-hot-reload';
|
|
11
|
+
import { useRenderingMetadata } from '../hooks/use-rendering-metadata';
|
|
12
|
+
|
|
13
|
+
export const PreviewContext = createContext<
|
|
14
|
+
| {
|
|
15
|
+
renderedEmailMetadata: RenderedEmailMetadata | undefined;
|
|
16
|
+
renderingResult: EmailRenderingResult;
|
|
17
|
+
|
|
18
|
+
emailSlug: string;
|
|
19
|
+
emailPath: string;
|
|
20
|
+
}
|
|
21
|
+
| undefined
|
|
22
|
+
>(undefined);
|
|
23
|
+
|
|
24
|
+
interface PreviewProvider {
|
|
25
|
+
emailSlug: string;
|
|
26
|
+
emailPath: string;
|
|
27
|
+
|
|
28
|
+
serverRenderingResult: EmailRenderingResult;
|
|
29
|
+
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const PreviewProvider = ({
|
|
34
|
+
emailSlug,
|
|
35
|
+
emailPath,
|
|
36
|
+
serverRenderingResult,
|
|
37
|
+
children,
|
|
38
|
+
}: PreviewProvider) => {
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
|
|
41
|
+
const renderingResult = useEmailRenderingResult(
|
|
42
|
+
emailPath,
|
|
43
|
+
serverRenderingResult,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const renderedEmailMetadata = useRenderingMetadata(
|
|
47
|
+
emailPath,
|
|
48
|
+
renderingResult,
|
|
49
|
+
serverRenderingResult,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!isBuilding) {
|
|
53
|
+
// this will not change on runtime so it doesn't violate
|
|
54
|
+
// the rules of hooks
|
|
55
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
56
|
+
useHotreload((changes) => {
|
|
57
|
+
const changeForThisEmail = changes.find((change) =>
|
|
58
|
+
change.filename.includes(emailSlug),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (typeof changeForThisEmail !== 'undefined') {
|
|
62
|
+
if (changeForThisEmail.event === 'unlink') {
|
|
63
|
+
router.push('/');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<PreviewContext.Provider
|
|
71
|
+
value={{
|
|
72
|
+
emailPath,
|
|
73
|
+
emailSlug,
|
|
74
|
+
renderedEmailMetadata,
|
|
75
|
+
renderingResult,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</PreviewContext.Provider>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type EmailRenderingResult,
|
|
5
5
|
renderEmailByPath,
|
|
6
6
|
} from '../actions/render-email-by-path';
|
|
7
|
+
import { isBuilding } from '../app/env';
|
|
7
8
|
import { useHotreload } from './use-hot-reload';
|
|
8
9
|
|
|
9
10
|
export const useEmailRenderingResult = (
|
|
@@ -14,7 +15,7 @@ export const useEmailRenderingResult = (
|
|
|
14
15
|
serverEmailRenderedResult,
|
|
15
16
|
);
|
|
16
17
|
|
|
17
|
-
if (
|
|
18
|
+
if (!isBuilding) {
|
|
18
19
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
19
20
|
useHotreload(async (changes) => {
|
|
20
21
|
for await (const change of changes) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> </div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px
|
|
3
|
+
exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> </div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">Hello <!-- -->alanturing<!-- -->,</p><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="invited you to" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12"/></td><td align="left" data-id="__react-email-column"><img height="64" src="/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px"><tbody><tr><td><a href="https://vercel.com/teams/invite/foo" style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>  </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>  ​</i><![endif]--></span></a></td></tr></tbody></table><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com/teams/invite/foo" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com/teams/invite/foo</a></p><hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea"/><p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-bottom:16px;margin-top:16px">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch with us.</p></td></tr></tbody></table><!--/$--></body></html>"`;
|