1import { css } from '@emotion/react';
2import { SnackLogo } from '@expo/styleguide';
3import { ArrowUpRightIcon } from '@expo/styleguide-icons';
4import { useEffect, useRef, useState, PropsWithChildren } from 'react';
5
6import { Snippet } from '../Snippet';
7import { SnippetAction } from '../SnippetAction';
8import { SnippetContent } from '../SnippetContent';
9import { SnippetHeader } from '../SnippetHeader';
10
11import { SNACK_URL, getSnackFiles } from '~/common/snack';
12import { PageApiVersionContextType, usePageApiVersion } from '~/providers/page-api-version';
13import versions from '~/public/static/constants/versions.json';
14
15const DEFAULT_PLATFORM = 'android';
16const { LATEST_VERSION } = versions;
17
18type Props = PropsWithChildren<{
19  dependencies: string[];
20  label?: string;
21  defaultPlatform?: string;
22  templateId?: string;
23  files?: Record<string, string>;
24  platforms?: string[];
25  buttonTitle?: string;
26  contentHidden?: boolean;
27}>;
28
29export const SnackInline = ({
30  dependencies = [],
31  label,
32  defaultPlatform,
33  templateId,
34  files,
35  platforms,
36  buttonTitle,
37  contentHidden,
38  children,
39}: Props) => {
40  const contentRef = useRef<HTMLDivElement>(null);
41  const [isReady, setReady] = useState(false);
42  const context = usePageApiVersion();
43
44  useEffect(() => setReady(true), []);
45
46  // Filter out `latest` and use the concrete latest version instead. We want to
47  // keep `unversioned` in for the selected docs version though. This is used to
48  // find the examples in the static dir, and we don't have a `latest` version
49  // there, but we do have `unversioned`.
50  const getSelectedDocsVersion = () => {
51    const { version } = context as PageApiVersionContextType;
52    return version === 'latest' ? LATEST_VERSION : version;
53  };
54
55  // Get a SDK version that Snack will understand. `latest` and `unversioned`
56  // are meaningless to Snack so we filter those out and use `LATEST_VERSION` instead
57  const getSnackSdkVersion = () => {
58    let version = getSelectedDocsVersion();
59    if (version === 'unversioned') {
60      version = LATEST_VERSION;
61    }
62
63    return version.replace('v', '');
64  };
65
66  const getExamplesPath = () => {
67    return `${document.location.origin}/static/examples/${getSelectedDocsVersion()}`;
68  };
69
70  const getCode = () => {
71    const code = contentRef.current ? contentRef.current.textContent || '' : '';
72    return code.replace(/%%placeholder-start%%.*%%placeholder-end%%/g, '');
73  };
74
75  return (
76    <Snippet css={inlineSnackWrapperStyle} className="flex flex-col mb-3">
77      <SnippetHeader title={label || 'Example'}>
78        <form action={SNACK_URL} method="POST" target="_blank">
79          <input type="hidden" name="platform" value={defaultPlatform || DEFAULT_PLATFORM} />
80          <input type="hidden" name="name" value={label || 'Example'} />
81          <input type="hidden" name="dependencies" value={dependencies.join(',')} />
82          <input type="hidden" name="sdkVersion" value={getSnackSdkVersion()} />
83          {platforms && (
84            <input type="hidden" name="supportedPlatforms" value={platforms.join(',')} />
85          )}
86          {isReady && (
87            <input
88              type="hidden"
89              name="files"
90              value={JSON.stringify(
91                getSnackFiles({
92                  templateId,
93                  code: getCode(),
94                  files,
95                  baseURL: getExamplesPath(),
96                })
97              )}
98            />
99          )}
100          <SnippetAction
101            disabled={!isReady}
102            icon={<SnackLogo />}
103            iconRight={<ArrowUpRightIcon className="icon-sm text-icon-secondary" />}
104            type="submit">
105            {buttonTitle || 'Open in Snack'}
106          </SnippetAction>
107        </form>
108      </SnippetHeader>
109      <SnippetContent ref={contentRef} css={contentHidden && css({ display: 'none' })} skipPadding>
110        {children}
111      </SnippetContent>
112    </Snippet>
113  );
114};
115
116const inlineSnackWrapperStyle = css({
117  pre: {
118    margin: 0,
119    border: 0,
120  },
121});
122