Skip to content

Commit 5c51172

Browse files
authored
feat: add embed modal (#240)
1 parent b97fd2b commit 5c51172

File tree

8 files changed

+270
-76
lines changed

8 files changed

+270
-76
lines changed

src/components/Embed.js

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React, { useState } from 'react';
2+
import Input from './Input';
3+
import CopyButton from './CopyButton';
4+
import Embedded from './Embedded';
5+
import { XIcon } from '@primer/octicons-react';
6+
7+
function TabButton({ children, active, onClick, disabled }) {
8+
return (
9+
<button
10+
disabled={disabled}
11+
className={[
12+
'text-xs select-none border-b-2',
13+
disabled ? '' : 'hover:text-blue-400 hover:border-blue-400',
14+
active
15+
? 'border-blue-600 text-blue-600'
16+
: 'border-transparent text-gray-800',
17+
].join(' ')}
18+
onClick={disabled ? undefined : onClick}
19+
>
20+
{children}
21+
</button>
22+
);
23+
}
24+
25+
const possiblePanes = ['markup', 'preview', 'query', 'result'];
26+
27+
function Embed({ dirty, gistId, gistVersion }) {
28+
const [panes, setPanes] = useState(['preview', 'result']);
29+
30+
const width = 850;
31+
const height = 300;
32+
33+
const embedUrl =
34+
[location.origin, 'embed', gistId, gistVersion].filter(Boolean).join('/') +
35+
`?panes=${panes.join(',')}`;
36+
37+
const embedCode = `<iframe src="${embedUrl}" height="${height}" width="100%" scrolling="no" frameBorder="0" allowTransparency="true" title="Testing Playground" style="overflow: hidden; display: block; width: 100%"></iframe>`;
38+
const canAddPane = panes.length < 3;
39+
40+
return (
41+
<div className="settings text-sm pb-2">
42+
<div className="space-y-6">
43+
{dirty && (
44+
<section className="bg-blue-100 p-2 text-xs rounded my-2 text-blue-800">
45+
Please note that this playground has
46+
<strong> unsaved changes </strong>. The embed
47+
<strong> will not include </strong> your latest changes!
48+
</section>
49+
)}
50+
51+
<section className="flex flex-col space-y-4">
52+
<div className="flex items-center justify-between">
53+
<h3 className="text-sm font-bold">Configure</h3>
54+
<TabButton
55+
onClick={() =>
56+
setPanes([
57+
...panes,
58+
possiblePanes.find((x) => !panes.includes(x)),
59+
])
60+
}
61+
disabled={!canAddPane}
62+
>
63+
add pane
64+
</TabButton>
65+
</div>
66+
67+
<div className="bg-gray-200 px-4 pb-4 -mx-4">
68+
<div className="px-4 gap-4 grid grid-flow-col py-1">
69+
{panes.map((selected, idx) => (
70+
<div key={idx} className="flex items-center justify-between">
71+
<div className="text-left space-x-2">
72+
{possiblePanes.map((name) => (
73+
<TabButton
74+
key={name}
75+
onClick={() =>
76+
setPanes((current) => {
77+
const next = [...current];
78+
next[idx] = name;
79+
return next;
80+
})
81+
}
82+
active={selected === name}
83+
>
84+
{name}
85+
</TabButton>
86+
))}
87+
</div>
88+
<TabButton
89+
disabled={panes.length === 1}
90+
onClick={() => setPanes(panes.filter((_, i) => i !== idx))}
91+
>
92+
<span>
93+
<XIcon size={12} />
94+
</span>
95+
</TabButton>
96+
</div>
97+
))}
98+
</div>
99+
100+
<div style={{ width, height }}>
101+
<Embedded
102+
panes={panes}
103+
gistId={gistId}
104+
gistVersion={gistVersion}
105+
/>
106+
</div>
107+
</div>
108+
</section>
109+
110+
<section className="flex flex-col space-y-4" style={{ width }}>
111+
<h3 className="text-sm font-bold">Copy & Paste</h3>
112+
113+
<label className="text-xs">
114+
embed link:
115+
<div className="flex space-x-4">
116+
<Input value={embedUrl} onChange={() => {}} readOnly name="url" />
117+
<CopyButton text={embedUrl} />
118+
</div>
119+
</label>
120+
121+
<label className="text-xs">
122+
embed code:
123+
<div className="w-full flex space-x-4">
124+
<code className="p-4 rounded bg-gray-200 text-gray-800 font-mono text-xs">
125+
{embedCode}
126+
</code>
127+
<CopyButton text={embedCode} />
128+
</div>
129+
</label>
130+
</section>
131+
</div>
132+
</div>
133+
);
134+
}
135+
136+
export default Embed;

src/components/Embedded.js

+82-53
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Query from './Query';
66
import Result from './Result';
77
import MarkupEditor from './MarkupEditor';
88
import usePlayground from '../hooks/usePlayground';
9+
import Loader from './Loader';
910

1011
const SUPPORTED_PANES = {
1112
markup: true,
@@ -23,16 +24,23 @@ const styles = {
2324
};
2425

2526
// TODO: we should support readonly mode
26-
function Embedded() {
27-
const { gistId, gistVersion } = useParams();
28-
const [state, dispatch] = usePlayground({ gistId, gistVersion });
29-
const { markup, query, result } = state;
27+
function Embedded(props) {
28+
const params = useParams();
29+
const [state, dispatch] = usePlayground({
30+
gistId: props.gistId || params.gistId,
31+
gistVersion: props.gistVersion || params.gistVersion,
32+
});
33+
const { markup, query, result, status } = state;
34+
const isLoading = status === 'loading';
3035

3136
const location = useLocation();
32-
const params = queryString.parse(location.search);
37+
const searchParams = queryString.parse(location.search);
3338

34-
const panes = params.panes
35-
? Array.from(new Set(params.panes.split(',')))
39+
const panes = props.panes
40+
? props.panes
41+
: searchParams.panes
42+
? searchParams.panes
43+
.split(',')
3644
.map((x) => x.trim())
3745
.filter((x) => SUPPORTED_PANES[x])
3846
: ['markup', 'preview', 'query', 'result'];
@@ -51,58 +59,79 @@ function Embedded() {
5159
: 'grid-cols-1';
5260

5361
useEffect(() => {
62+
if (window === top) {
63+
return;
64+
}
65+
5466
document.body.classList.add('embedded');
5567
return () => document.body.classList.remove('embedded');
5668
}, []);
5769

5870
return (
59-
<div
60-
className={`h-full overflow-hidden grid grid-flow-col gap-4 p-4 bg-white shadow rounded ${columnClass}`}
61-
>
62-
{/*the sandbox must always be rendered!*/}
63-
{!panes.includes('preview') && (
64-
<div style={styles.offscreen}>
65-
<Preview
66-
markup={markup}
67-
elements={result?.elements}
68-
accessibleRoles={result?.accessibleRoles}
69-
dispatch={dispatch}
70-
/>
71-
</div>
72-
)}
71+
<div className="relative w-full h-full">
72+
<Loader loading={isLoading} />
73+
<div
74+
className={[
75+
`h-full overflow-hidden grid grid-flow-col gap-4 p-4 bg-white shadow rounded fade`,
76+
columnClass,
77+
isLoading ? 'opacity-0' : 'opacity-100',
78+
].join(' ')}
79+
>
80+
{/*the sandbox must always be rendered!*/}
81+
{!panes.includes('preview') && (
82+
<div style={styles.offscreen}>
83+
<Preview
84+
markup={markup}
85+
elements={result?.elements}
86+
accessibleRoles={result?.accessibleRoles}
87+
dispatch={dispatch}
88+
/>
89+
</div>
90+
)}
7391

74-
{panes.map((area) => {
75-
switch (area) {
76-
case 'preview':
77-
return (
78-
<Preview
79-
key={area}
80-
markup={markup}
81-
elements={result?.elements}
82-
accessibleRoles={result?.accessibleRoles}
83-
dispatch={dispatch}
84-
/>
85-
);
86-
case 'markup':
87-
return (
88-
<MarkupEditor key={area} markup={markup} dispatch={dispatch} />
89-
);
90-
case 'query':
91-
return (
92-
<Query
93-
key={area}
94-
query={query}
95-
result={result}
96-
dispatch={dispatch}
97-
variant="minimal"
98-
/>
99-
);
100-
case 'result':
101-
return <Result key={area} result={result} dispatch={dispatch} />;
102-
default:
103-
return null;
104-
}
105-
})}
92+
{panes.map((area, idx) => {
93+
switch (area) {
94+
case 'preview':
95+
return (
96+
<Preview
97+
key={`${area}-${idx}`}
98+
markup={markup}
99+
elements={result?.elements}
100+
accessibleRoles={result?.accessibleRoles}
101+
dispatch={dispatch}
102+
/>
103+
);
104+
case 'markup':
105+
return (
106+
<MarkupEditor
107+
key={`${area}-${idx}`}
108+
markup={markup}
109+
dispatch={dispatch}
110+
/>
111+
);
112+
case 'query':
113+
return (
114+
<Query
115+
key={`${area}-${idx}`}
116+
query={query}
117+
result={result}
118+
dispatch={dispatch}
119+
variant="minimal"
120+
/>
121+
);
122+
case 'result':
123+
return (
124+
<Result
125+
key={`${area}-${idx}`}
126+
result={result}
127+
dispatch={dispatch}
128+
/>
129+
);
130+
default:
131+
return null;
132+
}
133+
})}
134+
</div>
106135
</div>
107136
);
108137
}

src/components/Header.js

+19-6
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ import {
1414
SyncIcon,
1515
UploadIcon,
1616
RepoForkedIcon,
17+
CodeIcon,
1718
} from '@primer/octicons-react';
1819
import Settings from './Settings';
1920
import Share from './Share';
21+
import Embed from './Embed';
22+
2023
const headerLinks = [
2124
links.testing_library_docs,
2225
links.which_query,
@@ -25,6 +28,7 @@ const headerLinks = [
2528

2629
function Header({
2730
gistId,
31+
gistVersion,
2832
status,
2933
dirty,
3034
canSave,
@@ -98,12 +102,21 @@ function Header({
98102
</ModalContents>
99103
</Modal>
100104

101-
{/*reserved for future implementation, see #54 */}
102-
{/*reserved for future implementation*/}
103-
{/*<MenuLink as="button">*/}
104-
{/* <CodeIcon size={12} />*/}
105-
{/* <span>Embed</span>*/}
106-
{/*</MenuLink>*/}
105+
<Modal>
106+
<ModalOpenButton>
107+
<MenuLink as="button">
108+
<CodeIcon size={12} />
109+
<span>Embed</span>
110+
</MenuLink>
111+
</ModalOpenButton>
112+
<ModalContents>
113+
<Embed
114+
dirty={dirty}
115+
gistId={gistId}
116+
gistVersion={gistVersion}
117+
/>
118+
</ModalContents>
119+
</Modal>
107120
</MenuList>
108121
</Menu>
109122

src/components/Layout.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@ import React from 'react';
22
import Header from './Header';
33
import { ToastContainer } from 'react-toastify';
44

5-
function Layout({ children, dirty, gistId, dispatch, status, settings }) {
5+
function Layout({
6+
children,
7+
dirty,
8+
gistId,
9+
gistVersion,
10+
dispatch,
11+
status,
12+
settings,
13+
}) {
614
return (
715
<div className="flex flex-col h-screen">
816
<div className="mb-8 flex-none">
917
<Header
1018
gistId={gistId}
19+
gistVersion={gistVersion}
1120
dirty={dirty}
1221
canSave={!!dirty}
1322
canFork={!!gistId}

src/components/Loader.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
import frog from 'url:~/public/128-production.png';
3+
4+
function Loader({ loading }) {
5+
return (
6+
<div
7+
className={[
8+
'w-full h-full absolute top-0 left-0 flex flex-col justify-center items-center w-full h-full space-y-4 fade',
9+
loading ? 'opacity-100' : 'opacity-0',
10+
].join(' ')}
11+
>
12+
<img className="opacity-50" src={frog} />
13+
</div>
14+
);
15+
}
16+
17+
export default Loader;

0 commit comments

Comments
 (0)