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