Skip to content

Commit b621aec

Browse files
authored
feat(web): Improved way of switching to pure HTML (#1701)
1 parent 0c0a163 commit b621aec

File tree

4 files changed

+288
-200
lines changed

4 files changed

+288
-200
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import * as React from "react";
2+
import * as Select from "@radix-ui/react-select";
3+
import {
4+
CheckIcon,
5+
ChevronDownIcon,
6+
ChevronUpIcon,
7+
ClipboardIcon,
8+
} from "lucide-react";
9+
import * as Tabs from "@radix-ui/react-tabs";
10+
import type {
11+
CodeVariant,
12+
ImportedComponent,
13+
} from "@/app/components/get-components";
14+
import { useStoredState } from "@/hooks/use-stored-state";
15+
import { CodeBlock } from "./code-block";
16+
import { TabTrigger } from "./tab-trigger";
17+
18+
type ReactCodeVariant = Exclude<CodeVariant, "html" | "react">;
19+
20+
const srcAttributeRegex = /src\s*=\s*"(?<URI>\/static.+)"/gm;
21+
22+
export const ComponentCodeView = ({
23+
component,
24+
}: {
25+
component: ImportedComponent;
26+
}) => {
27+
const [selectedReactCodeVariant, setSelectedReactCodeVariant] =
28+
useStoredState<ReactCodeVariant>("code-variant", "tailwind");
29+
30+
const [selectedLanguage, setSelectedLanguage] = useStoredState<
31+
"html" | "react"
32+
>("code-language", "react");
33+
34+
const [isCopied, setIsCopied] = React.useState<boolean>(false);
35+
36+
let code = component.code.html;
37+
if (
38+
selectedLanguage === "react" &&
39+
selectedReactCodeVariant in component.code
40+
) {
41+
// Resets the regex so that it doesn't break in subsequent runs
42+
srcAttributeRegex.lastIndex = 0;
43+
code = component.code[selectedReactCodeVariant] ?? "";
44+
}
45+
code = code.replaceAll(
46+
srcAttributeRegex,
47+
(_match, uri) => `src="https://react.email${uri}"`,
48+
);
49+
50+
const onCopy = () => {
51+
void navigator.clipboard.writeText(code);
52+
setIsCopied(true);
53+
54+
setTimeout(() => {
55+
setIsCopied(false);
56+
}, 1000);
57+
};
58+
59+
const handleKeyUp: React.KeyboardEventHandler<HTMLButtonElement> = (
60+
event,
61+
) => {
62+
if (event.key === "Enter" || event.key === " ") {
63+
onCopy();
64+
}
65+
};
66+
67+
return (
68+
<div className="flex h-full w-full flex-col gap-2 bg-slate-3">
69+
<div className="relative flex w-full justify-between gap-4 border-b border-solid border-slate-4 p-4 text-xs">
70+
<Tabs.Root
71+
defaultValue={selectedLanguage}
72+
onValueChange={(v) => {
73+
setSelectedLanguage(v as "react" | "html");
74+
}}
75+
value={selectedLanguage}
76+
>
77+
<Tabs.List className="p1-text-xs flex w-fit space-x-1 overflow-hidden">
78+
<TabTrigger
79+
activeView={selectedLanguage}
80+
layoutId={`${component.slug}-language`}
81+
value="html"
82+
>
83+
HTML
84+
</TabTrigger>
85+
<TabTrigger
86+
activeView={selectedLanguage}
87+
layoutId={`${component.slug}-language`}
88+
value="react"
89+
>
90+
React
91+
</TabTrigger>
92+
</Tabs.List>
93+
</Tabs.Root>
94+
<div className="flex gap-2">
95+
{selectedLanguage === "react" ? (
96+
<ReactVariantSelect
97+
onChange={(newValue) => {
98+
localStorage.setItem("code-variant", newValue);
99+
setSelectedReactCodeVariant(newValue);
100+
}}
101+
value={selectedReactCodeVariant}
102+
/>
103+
) : null}
104+
<button
105+
aria-label="Copy code"
106+
className="flex h-8 w-8 items-center justify-center rounded-sm outline-0 focus-within:ring-2 focus-within:ring-slate-6 focus-within:ring-opacity-50"
107+
onClick={onCopy}
108+
onKeyUp={handleKeyUp}
109+
tabIndex={0}
110+
type="button"
111+
>
112+
{isCopied ? <CheckIcon size={16} /> : <ClipboardIcon size={16} />}
113+
</button>
114+
</div>
115+
</div>
116+
<div className="h-full w-full overflow-auto">
117+
<CodeBlock language={selectedLanguage === "html" ? "html" : "tsx"}>
118+
{code}
119+
</CodeBlock>
120+
</div>
121+
</div>
122+
);
123+
};
124+
125+
const ReactVariantSelect = ({
126+
value,
127+
onChange,
128+
}: {
129+
value: ReactCodeVariant;
130+
onChange: (newValue: ReactCodeVariant) => void;
131+
}) => {
132+
return (
133+
<Select.Root
134+
onValueChange={(variant: ReactCodeVariant) => {
135+
onChange(variant);
136+
}}
137+
value={value}
138+
>
139+
<Select.Trigger
140+
aria-label="Choose the styling solution"
141+
className="flex h-8 items-center justify-center gap-1 rounded bg-slate-3 px-3 leading-none outline-none focus-within:ring-2 focus-within:ring-slate-6 focus-within:ring-opacity-50 data-[placeholder]:text-slate-11"
142+
>
143+
<Select.Value>
144+
{(() => {
145+
if (value === "tailwind") {
146+
return "Tailwind CSS";
147+
}
148+
149+
return "Inline CSS";
150+
})()}
151+
</Select.Value>
152+
<Select.Icon>
153+
<ChevronDownIcon size={14} />
154+
</Select.Icon>
155+
</Select.Trigger>
156+
<Select.Portal>
157+
<Select.Content className="z-[2] overflow-hidden rounded-md bg-[#1F2122]">
158+
<Select.ScrollUpButton className="flex h-6 cursor-default items-center justify-center">
159+
<ChevronUpIcon size={12} />
160+
</Select.ScrollUpButton>
161+
<Select.Viewport className="p-1">
162+
{["tailwind", "inline-styles"].map((variant) => (
163+
<Select.Item
164+
className="relative flex h-8 cursor-pointer select-none items-center rounded-[.25rem] px-6 py-2 text-xs leading-none text-slate-11 transition-colors ease-[cubic-bezier(.36,.66,.6,1)] data-[disabled]:pointer-events-none data-[highlighted]:bg-slate-3 data-[highlighted]:text-slate-12 data-[highlighted]:outline-none"
165+
key={variant}
166+
value={variant}
167+
>
168+
<Select.ItemText>
169+
{(() => {
170+
if (variant === "tailwind") {
171+
return "Tailwind CSS";
172+
}
173+
174+
return "Inline CSS";
175+
})()}
176+
</Select.ItemText>
177+
<Select.ItemIndicator className="absolute left-0 inline-flex w-6 items-center justify-center text-slate-12">
178+
<CheckIcon size={10} />
179+
</Select.ItemIndicator>
180+
</Select.Item>
181+
))}
182+
</Select.Viewport>
183+
</Select.Content>
184+
</Select.Portal>
185+
</Select.Root>
186+
);
187+
};

0 commit comments

Comments
 (0)