Skip to content

Commit

Permalink
feat: gist sharing, format code on blur
Browse files Browse the repository at this point in the history
  • Loading branch information
reme3d2y committed Jul 11, 2024
1 parent 3d47c69 commit d9beb23
Show file tree
Hide file tree
Showing 9 changed files with 360 additions and 104 deletions.
4 changes: 3 additions & 1 deletion .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { CSF } from '../stories/CSF';

addons.setConfig({
[LIVE_EXAMPLES_ADDON_ID]: {
sandboxPath: '/docs/sandbox--page',
sandboxPath: '/docs/sandbox--docs',
mobileFrameName: 'internalmobileframe--docs',
githubToken: '',
shareMode: 'url',
previewBgColor: '#ffffff',
scope: {
Button,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@storybook/react-webpack5": "^7.0.12",
"@types/prettier": "^2.3.2",
"@types/react-dom": "^17.0.9",
"auto": "^10.3.0",
"auto": "^11.0.5",
"babel-loader": "^8.1.0",
"boxen": "^5.0.1",
"concurrently": "^6.2.0",
Expand Down
6 changes: 4 additions & 2 deletions src/components/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { IconButton, IconButtonProps } from '@alfalab/core-components/icon-button';
import { Toast, ToastProps } from '@alfalab/core-components/toast';
import { styled } from '@storybook/theming';
import React, { FC, forwardRef, useState } from 'react';
import React, { FC, ReactNode, forwardRef, useState } from 'react';
import { configValue } from '../config';

export type ActionButtonProps = IconButtonProps & {
onClick?: () => void;
active?: boolean;
ref?: React.Ref<HTMLButtonElement>;
doneTitle?: string;
doneTitle?: string & ReactNode;
toastProps?: Partial<ToastProps>;
};

Expand Down Expand Up @@ -42,9 +42,11 @@ export const ActionButton: FC<ActionButtonProps> = forwardRef(
block={false}
onClose={() => setOpen(false)}
autoCloseDelay={1500}
{...toastProps}
style={{
left: '50%',
transform: 'translateX(-50%)',
...toastProps?.style,
}}
/>
)}
Expand Down
25 changes: 11 additions & 14 deletions src/components/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@ import { styled } from '@storybook/theming';
import { DisplayMIcon } from '@alfalab/icons-glyph/DisplayMIcon';
import { MobilePhoneLineMIcon } from '@alfalab/icons-glyph/MobilePhoneLineMIcon';
import { CopyLineMIcon } from '@alfalab/icons-glyph/CopyLineMIcon';
import { ShareMIcon } from '@alfalab/icons-glyph/ShareMIcon';
import { RepeatMIcon } from '@alfalab/icons-glyph/RepeatMIcon';

import { extractLanguageFromClassName, detectNoInline, copyToClipboard, uniqId } from './utils';
import { ActionButton } from './ActionButton';
import { ActionBar } from './ActionBar';
import { LOADED_MESSAGE } from './MobileFrame';
import { useCode } from './useCode';
import { formatCode, useCode } from './useCode';
import ExpandMIcon from './icons/ExpandMIcon';
import { configValue, getConfig } from '../config';
import { CUSTOM_EVENTS, dispatchCustomEvent } from './events';
import { ShareButton } from './ShareButton';

export type ExampleProps = {
live?: boolean;
Expand Down Expand Up @@ -173,7 +172,7 @@ export const Example: FC<ExampleProps & { example?: number }> = ({

const frameRef = useRef<HTMLIFrameElement>();

const { code, setCode, resetCode, resetKey, ready } = useCode({
const { code, setCode, resetCode, resetKey, setResetKey, ready } = useCode({
initialCode: codeProp,
desktopOnly,
mobileOnly,
Expand Down Expand Up @@ -201,6 +200,11 @@ export const Example: FC<ExampleProps & { example?: number }> = ({
setCode(value.trim());
};

const handleBlur = () => {
setCode(formatCode(code));
setResetKey(+new Date());
};

const handleViewChange = (view: 'mobile' | 'desktop') => () => {
setView(view);
dispatchCustomEvent(CUSTOM_EVENTS.VIEW_CHANGE, { view });
Expand Down Expand Up @@ -330,16 +334,8 @@ export const Example: FC<ExampleProps & { example?: number }> = ({
/>

{allowShare && (
<ActionButton
icon={ShareMIcon}
onClick={() => {
handleCopy(
`${window.parent.location.origin}${
window.parent.location.pathname
}?path=${sandboxPath}/code=${encodeURIComponent(code)}`,
);
dispatchCustomEvent(CUSTOM_EVENTS.SHARE);
}}
<ShareButton
code={code}
title={configValue('shareText', 'Share code')}
doneTitle={configValue('sharedText', 'Link copied')}
disabled={viewMismatch}
Expand Down Expand Up @@ -420,6 +416,7 @@ export const Example: FC<ExampleProps & { example?: number }> = ({
language={language}
disabled={!live}
key={`${view}_${resetKey}`}
onBlur={handleBlur}
data-role='editor'
/>
</LiveEditorWrapper>
Expand Down
53 changes: 53 additions & 0 deletions src/components/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { FC, ReactNode } from 'react';
import { Spinner } from '@alfalab/core-components/spinner';

import { ActionButton } from './ActionButton';
import { ShareMIcon } from '@alfalab/icons-glyph/ShareMIcon';
import { copyToClipboard } from './utils';
import { share } from './utils/share';
import { CUSTOM_EVENTS, dispatchCustomEvent } from './events';

export type ActionButtonProps = {
code?: string;
disabled?: boolean;
title?: string;
doneTitle?: string;
};

export const ShareButton: FC<ActionButtonProps> = ({ code, disabled, title, doneTitle }) => {
const [toastOpen, setToastOpen] = React.useState(false);
const [toastText, setToastText] = React.useState<ReactNode>('');

const handleShare = async () => {
dispatchCustomEvent(CUSTOM_EVENTS.SHARE);

setToastOpen(true);
setToastText(<Spinner visible={true} colors='inverted' />);

try {
const url = await share(code);
copyToClipboard(url);
setToastText(doneTitle);
} catch (error) {
setToastText(error.message);
} finally {
setTimeout(() => {
setToastOpen(false);
}, 500);
}
};

return (
<ActionButton
icon={ShareMIcon}
onClick={() => handleShare()}
title={title}
doneTitle={toastText as string}
disabled={disabled || toastOpen}
toastProps={{
autoCloseDelay: 0,
open: toastOpen,
}}
/>
);
};
29 changes: 19 additions & 10 deletions src/components/useCode.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import prettier from 'prettier/standalone';
import parserBabel from 'prettier/parser-babel';
import { transpileTs } from './utils';
import { Language } from 'prism-react-renderer';
import { dispatchCustomEvent, CUSTOM_EVENTS } from './events';
import { loadGist } from './utils/share';
import { CUSTOM_EVENTS, dispatchCustomEvent } from './events';

type UseCodeProps = {
initialCode: string;
Expand All @@ -17,11 +18,10 @@ type UseCodeProps = {

const CHUNK_SEPARATOR = /^\s*(?:@|\/\/)MOBILE@?/m;

const transpile = (code: string) => {
return transpileTs(code).then((jsCode) =>
prettier.format(jsCode, { parser: 'babel', plugins: [parserBabel] }),
);
};
export const formatCode = (code: string) =>
prettier.format(code, { parser: 'babel', plugins: [parserBabel] });

const transpile = (code: string) => transpileTs(code).then((jsCode) => formatCode(jsCode));

export function useCode({
initialCode,
Expand All @@ -42,15 +42,23 @@ export function useCode({
const [desktopInitialCode, setDesktopInitialCode] = useState('');
const [mobileInitialCode, setMobileInitialCode] = useState('');

const initialCodeRef = useRef<string>();

const useCommonCode = CHUNK_SEPARATOR.exec(initialCode) === null;

const reset = () => {
setResetKey(+new Date());
dispatchCustomEvent(CUSTOM_EVENTS.REFRESH);
};

const prepareCode = () => {
Promise.all(
const prepareCode = async () => {
if (/gist:\w+/.test(initialCode)) {
initialCode = await loadGist(initialCode.split(':')[1]);
}

initialCodeRef.current = initialCode;

await Promise.all(
initialCode
.split(CHUNK_SEPARATOR)
.map((s) => s.trim())
Expand All @@ -77,7 +85,7 @@ export function useCode({
let setCode = setCommonCode;
let resetCode = () => {
reset();
setCommonCode(initialCode);
setCommonCode(initialCodeRef.current);
};

if (!useCommonCode) {
Expand Down Expand Up @@ -105,6 +113,7 @@ export function useCode({
setCode,
resetCode,
resetKey,
setResetKey,
ready,
};
}
68 changes: 68 additions & 0 deletions src/components/utils/share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getConfig } from '../../config';

export async function share(code: string) {
const config = getConfig();

const { sandboxPath, shareMode, githubToken } = config;

if (shareMode === 'url') {
return `${window.parent.location.origin}?path=${sandboxPath}/code=${encodeURIComponent(
code,
)}`;
}

if (shareMode === 'gist') {
const id = await createGist(code, githubToken);
return `${window.parent.location.origin}?path=${sandboxPath}/code=gist:${id}`;
}
}

const FILENAME = 'example.js';

export async function createGist(code: string, token: string) {
const apiUrl = 'https://api.github.com/gists';

const data = {
description: 'Example from Storybook',
public: false,
files: {
[FILENAME]: {
content: code,
},
},
};

const response = await fetch(apiUrl, {
method: 'POST',
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

if (!response.ok) {
throw new Error(`Failed to create gist: ${response.statusText}`);
}

const result = await response.json();

return result.id;
}

export async function loadGist(id: string) {
const apiUrl = `https://api.github.com/gists/${id}`;

const response = await fetch(apiUrl);

if (!response.ok) {
throw new Error(`Failed to load gist: ${response.statusText}`);
}

const result = await response.json();
if ('message' in result) {
throw new Error(`${result.status}: ${result.message}`);
}

return result.files[FILENAME].content;
}
7 changes: 6 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export type Config = {

sandboxPath?: string;

mobileFrameName?: string;

desktopText?: string;
mobileText?: string;
expandText?: string;
Expand All @@ -41,10 +43,13 @@ export type Config = {

editorTheme?: PrismTheme;
scope: Record<string, any>;

shareMode?: 'gist' | 'url';
githubToken?: string;
};

export const getConfig = () => {
return addons.getConfig()[LIVE_EXAMPLES_ADDON_ID] || {};
return addons.getConfig()[LIVE_EXAMPLES_ADDON_ID] as Config;
};

export const configValue = <T extends keyof Config>(key: T, defaultValue: any): Config[T] => {
Expand Down
Loading

0 comments on commit d9beb23

Please sign in to comment.