@codecademy/codebytes 0.0.2-alpha.d83a50.0 → 0.1.1-alpha.675361.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ### 0.0.2-alpha.d83a50.0 (2021-12-17)
6
+ ### [0.1.1-alpha.675361.0](https://github.com/Codecademy/client-modules/compare/@codecademy/codebytes@0.1.0...@codecademy/codebytes@0.1.1-alpha.675361.0) (2022-01-03)
7
7
 
8
8
  **Note:** Version bump only for package @codecademy/codebytes
9
+
10
+
11
+
12
+
13
+
14
+ ## 0.1.0 (2021-12-17)
15
+
16
+
17
+ ### Features
18
+
19
+ * **Codebytes:** move codebytes parent disc 351 ([#11](https://github.com/Codecademy/client-modules/issues/11)) ([30edd2b](https://github.com/Codecademy/client-modules/commit/30edd2b7a0e50c27d3adcf231b56441b8e8f6b81))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@codecademy/codebytes",
3
3
  "description": "Codebytes Code Editor",
4
- "version": "0.0.2-alpha.d83a50.0",
4
+ "version": "0.1.1-alpha.675361.0",
5
5
  "author": "Codecademy Engineering <dev@codecademy.com>",
6
6
  "sideEffects": [
7
7
  "**/*.css",
@@ -23,17 +23,9 @@
23
23
  "@codecademy/gamut": "*",
24
24
  "@codecademy/gamut-icons": "*",
25
25
  "@codecademy/gamut-styles": "*",
26
+ "@codecademy/variance": "*",
26
27
  "@emotion/react": "^11.4.0",
27
- "@emotion/styled": "^11.3.0",
28
- "@storybook/addon-a11y": "6.3.4",
29
- "@storybook/addon-actions": "6.3.4",
30
- "@storybook/addon-controls": "6.3.4",
31
- "@storybook/addon-docs": "6.3.4",
32
- "@storybook/addon-essentials": "6.3.4",
33
- "@storybook/addon-links": "6.3.4",
34
- "@storybook/addons": "6.3.4",
35
- "@storybook/react": "6.3.4",
36
- "@storybook/theming": "6.3.4"
28
+ "@emotion/styled": "^11.3.0"
37
29
  },
38
30
  "scripts": {
39
31
  "verify": "tsc --noEmit",
@@ -41,28 +33,16 @@
41
33
  "build:clean": "rm -rf dist",
42
34
  "build:types": "tsc --emitDeclarationOnly",
43
35
  "build": "yarn build:clean && yarn build:compile && yarn build:types",
44
- "build:watch": "yarn build && onchange ./src -- yarn build:compile && yarn build:types",
45
- "lernaBuildTask": "yarn build",
46
- "storybook": "start-storybook -p 6006",
47
- "build-storybook": "build-storybook"
36
+ "build:watch": "yarn build && onchange ./src -- yarn build:compile && yarn build:types"
48
37
  },
49
38
  "license": "MIT",
50
39
  "devDependencies": {
51
40
  "@emotion/jest": "^11.3.0",
52
- "@storybook/addon-a11y": "^6.4.4",
53
- "@storybook/addon-actions": "^6.4.4",
54
- "@storybook/addon-controls": "^6.4.4",
55
- "@storybook/addon-docs": "^6.4.4",
56
- "@storybook/addon-essentials": "^6.4.4",
57
- "@storybook/addon-links": "^6.4.4",
58
- "@storybook/addons": "^6.4.4",
59
- "@storybook/react": "^6.4.4",
60
- "@storybook/theming": "^6.4.4",
61
41
  "@testing-library/dom": "^7.31.2",
62
42
  "@testing-library/react": "^11.0.4"
63
43
  },
64
44
  "publishConfig": {
65
45
  "access": "public"
66
46
  },
67
- "gitHead": "3e1e0013158d73c6f50cd60cd7cc71bbfa2008a5"
47
+ "gitHead": "d9c4df33e1b24cd2f123e939a0e4c2279096ba36"
68
48
  }
package/src/api.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { languageOption } from './consts';
2
+
3
+ interface Response {
4
+ stderr: string;
5
+ stdout: string;
6
+ exit_code: number;
7
+ }
8
+
9
+ interface PostSnippetData {
10
+ language: languageOption;
11
+ code: string;
12
+ }
13
+
14
+ export const postSnippet = async (
15
+ data: PostSnippetData,
16
+ snippetsBaseUrl?: string
17
+ ): Promise<Response> => {
18
+ const snippetsEndpoint = `https://${snippetsBaseUrl}/snippets`;
19
+
20
+ const response = await fetch(snippetsEndpoint, {
21
+ method: 'POST',
22
+ body: JSON.stringify(data),
23
+ headers: {
24
+ 'x-codecademy-user-id': 'codebytes-anon-user',
25
+ },
26
+ });
27
+ return response.json();
28
+ };
package/src/consts.ts ADDED
@@ -0,0 +1,15 @@
1
+ // key = language param to send to snippets service
2
+ // val = label in language selection drop down
3
+ export const languageOptions = {
4
+ '': 'Select your language',
5
+ cpp: 'C++',
6
+ csharp: 'C#',
7
+ golang: 'Go',
8
+ javascript: 'JavaScript',
9
+ php: 'PHP',
10
+ python: 'Python 3',
11
+ ruby: 'Ruby',
12
+ scheme: 'Scheme',
13
+ };
14
+
15
+ export type languageOption = keyof typeof languageOptions;
@@ -0,0 +1,132 @@
1
+ import { FlexBox, IconButton } from '@codecademy/gamut';
2
+ import {
3
+ ArrowChevronLeftIcon,
4
+ ArrowChevronRightIcon,
5
+ } from '@codecademy/gamut-icons';
6
+ import styled from '@emotion/styled';
7
+ import React, { useState } from 'react';
8
+
9
+ const DrawerLabel = styled.span`
10
+ padding: 0.875rem 0.5rem;
11
+ `;
12
+
13
+ const LeftDrawerIcon = styled(ArrowChevronLeftIcon)<{ open?: boolean }>`
14
+ transition: transform 0.2s ease-in-out;
15
+ `;
16
+ const RightDrawerIcon = LeftDrawerIcon.withComponent(ArrowChevronRightIcon);
17
+
18
+ const Drawer = styled(FlexBox)<{ open?: boolean; hideOnClose?: boolean }>`
19
+ position: relative;
20
+ ${({ open, hideOnClose }) => `
21
+ flex-basis: ${open ? '100%' : '0%'};
22
+ visibility: ${!open && hideOnClose ? 'hidden' : 'visible'};
23
+ transition: flex-basis 0.2s ${
24
+ open ? 'ease-out' : 'ease-in, visibility 0s 0.2s'
25
+ };
26
+
27
+ ${LeftDrawerIcon}, ${RightDrawerIcon} {
28
+ transform: rotateZ(${open ? '0' : '180'}deg)};
29
+ }
30
+ `}
31
+ `;
32
+
33
+ export type DrawersProps = {
34
+ leftChild: React.ReactNode;
35
+ rightChild: React.ReactNode;
36
+ };
37
+
38
+ export const Drawers: React.FC<DrawersProps> = ({ leftChild, rightChild }) => {
39
+ const [open, setOpen] = useState<'left' | 'right' | 'both'>('both');
40
+
41
+ let ariaLabelCodeButton = 'hide code';
42
+ let ariaLabelOutputButton = 'hide output';
43
+ let isLeftOpen = false;
44
+ let isRightOpen = false;
45
+
46
+ if (open === 'left') {
47
+ ariaLabelCodeButton = ariaLabelOutputButton = 'show output';
48
+ isLeftOpen = true;
49
+ } else if (open === 'right') {
50
+ ariaLabelCodeButton = ariaLabelOutputButton = 'show code';
51
+ isRightOpen = true;
52
+ }
53
+
54
+ return (
55
+ <>
56
+ <FlexBox>
57
+ <Drawer
58
+ open={!isRightOpen}
59
+ alignItems="center"
60
+ flexWrap="nowrap"
61
+ textAlign="left"
62
+ borderRight={1}
63
+ borderColor="gray-900"
64
+ px={8}
65
+ >
66
+ <IconButton
67
+ icon={LeftDrawerIcon}
68
+ variant="secondary"
69
+ size="small"
70
+ onClick={() =>
71
+ setOpen((state) => (state === 'both' ? 'right' : 'both'))
72
+ }
73
+ aria-label={ariaLabelCodeButton}
74
+ aria-controls="code-drawer"
75
+ aria-expanded={!isRightOpen}
76
+ />
77
+ <DrawerLabel id="code-drawer-label">Code</DrawerLabel>
78
+ </Drawer>
79
+ <Drawer
80
+ open={!isLeftOpen}
81
+ alignItems="center"
82
+ flexWrap="nowrap"
83
+ justifyContent="flex-end"
84
+ px={8}
85
+ >
86
+ <DrawerLabel id="output-drawer-label">Output</DrawerLabel>
87
+ <IconButton
88
+ icon={RightDrawerIcon}
89
+ variant="secondary"
90
+ size="small"
91
+ onClick={() =>
92
+ setOpen((state) => (state === 'both' ? 'left' : 'both'))
93
+ }
94
+ aria-label={ariaLabelOutputButton}
95
+ aria-controls="output-drawer"
96
+ aria-expanded={!isLeftOpen}
97
+ />
98
+ </Drawer>
99
+ </FlexBox>
100
+ <FlexBox
101
+ flexGrow={1}
102
+ borderY={1}
103
+ borderColor="gray-900"
104
+ alignItems="stretch"
105
+ overflow="hidden"
106
+ >
107
+ <Drawer
108
+ hideOnClose
109
+ id="code-drawer"
110
+ aria-labelledby="code-drawer-label"
111
+ open={!isRightOpen}
112
+ flexGrow={0}
113
+ overflow="hidden"
114
+ borderColor="gray-900"
115
+ borderStyleRight="solid"
116
+ borderWidthRight="thin"
117
+ >
118
+ {leftChild}
119
+ </Drawer>
120
+ <Drawer
121
+ hideOnClose
122
+ id="output-drawer"
123
+ aria-labelledby="output-drawer-label"
124
+ open={!isLeftOpen}
125
+ overflow="hidden"
126
+ >
127
+ {rightChild}
128
+ </Drawer>
129
+ </FlexBox>
130
+ </>
131
+ );
132
+ };
package/src/editor.tsx CHANGED
@@ -1,10 +1,150 @@
1
- import React from 'react';
1
+ import {
2
+ FillButton,
3
+ FlexBox,
4
+ Spinner,
5
+ TextButton,
6
+ ToolTip,
7
+ } from '@codecademy/gamut';
8
+ import { CopyIcon } from '@codecademy/gamut-icons';
9
+ import { theme } from '@codecademy/gamut-styles';
10
+ import styled from '@emotion/styled';
11
+ import React, { useState } from 'react';
12
+
13
+ import { postSnippet } from './api';
14
+ import type { languageOption } from './consts';
15
+ import { Drawers } from './drawers';
16
+
17
+ const Output = styled.pre<{ hasError: boolean }>`
18
+ width: 100%;
19
+ height: 100%;
20
+ margin: 0;
21
+ padding: 0 1rem;
22
+ font-family: Monaco;
23
+ font-size: 0.875rem;
24
+ overflow: auto;
25
+ ${({ hasError }) => `
26
+ color: ${hasError ? theme.colors.orange : theme.colors.white};
27
+ background-color: ${theme.colors['gray-900']};
28
+ `}
29
+ `;
30
+
31
+ const CopyIconStyled = styled(CopyIcon)`
32
+ margin-right: 0.5rem;
33
+ `;
34
+
35
+ const DOCKER_SIGTERM = 143;
2
36
 
3
37
  type EditorProps = {
38
+ hideCopyButton: boolean;
39
+ language: languageOption;
4
40
  text: string;
5
- onChange: (text: string) => void;
41
+ // eslint-disable-next-line react/no-unused-prop-types
42
+ onChange: (
43
+ text: string
44
+ ) => void /* TODO: Add onChange behavior in DISC-353 */;
45
+ onCopy?: (text: string, language: string) => void;
46
+ snippetsBaseUrl?: string;
6
47
  };
7
48
 
8
- export const Editor: React.FC<EditorProps> = ({ text, onChange }) => {
9
- return <textarea value={text} onChange={(e) => onChange(e.target.value)} />;
49
+ export const Editor: React.FC<EditorProps> = ({
50
+ language,
51
+ text,
52
+ hideCopyButton,
53
+ onCopy,
54
+ snippetsBaseUrl,
55
+ }) => {
56
+ const [output, setOutput] = useState('');
57
+ const [status, setStatus] = useState<'ready' | 'waiting' | 'error'>('ready');
58
+ const [isCodeByteCopied, setIsCodeByteCopied] = useState(false);
59
+ const onCopyClick = () => {
60
+ if (!isCodeByteCopied) {
61
+ navigator.clipboard
62
+ .writeText(text)
63
+ // eslint-disable-next-line no-console
64
+ .catch(() => console.error('Failed to copy'));
65
+ onCopy?.(
66
+ text,
67
+ language
68
+ ); /* TODO: pass in onCopyBBCodeblock behavior from the future version we migrate to Next.js */
69
+ setIsCodeByteCopied(true);
70
+ }
71
+ };
72
+
73
+ const setErrorStatusAndOutput = (message: string) => {
74
+ setOutput(message);
75
+ setStatus('error');
76
+ };
77
+
78
+ const handleSubmit = async () => {
79
+ if (text.trim().length === 0) {
80
+ return;
81
+ }
82
+ const data = {
83
+ language,
84
+ code: text,
85
+ };
86
+ setStatus('waiting');
87
+ setOutput('');
88
+
89
+ try {
90
+ const response = await postSnippet(data, snippetsBaseUrl);
91
+ if (response.stderr.length > 0) {
92
+ setErrorStatusAndOutput(response.stderr);
93
+ } else if (response.exit_code === DOCKER_SIGTERM) {
94
+ setErrorStatusAndOutput(
95
+ 'Your code took too long to return a result. Double check your code for any issues and try again!'
96
+ );
97
+ } else if (response.exit_code !== 0) {
98
+ setErrorStatusAndOutput('An unknown error occured.');
99
+ } else {
100
+ setOutput(response.stdout);
101
+ setStatus('ready');
102
+ }
103
+ } catch (error) {
104
+ setErrorStatusAndOutput('Error: ' + error);
105
+ }
106
+ };
107
+
108
+ return (
109
+ <>
110
+ <Drawers
111
+ leftChild={<div>{text}</div>}
112
+ rightChild={
113
+ <Output hasError={status === 'error'} aria-live="polite">
114
+ {output}
115
+ </Output>
116
+ }
117
+ />
118
+ <FlexBox
119
+ justifyContent={hideCopyButton ? 'flex-end' : 'space-between'}
120
+ pl={8}
121
+ >
122
+ {!hideCopyButton ? (
123
+ <ToolTip
124
+ id="codebyte-copied"
125
+ alignment="top-right"
126
+ mode="dark"
127
+ target={
128
+ <TextButton
129
+ variant="secondary"
130
+ onClick={onCopyClick}
131
+ onBlur={() => setIsCodeByteCopied(false)}
132
+ >
133
+ <CopyIconStyled aria-hidden="true" /> Copy Codebyte
134
+ </TextButton>
135
+ }
136
+ >
137
+ {isCodeByteCopied ? (
138
+ <span role="alert">Copied!</span>
139
+ ) : (
140
+ <span>Copy to your clipboard</span>
141
+ )}
142
+ </ToolTip>
143
+ ) : null}
144
+ <FillButton onClick={handleSubmit}>
145
+ {status === 'waiting' ? <Spinner /> : 'Run'}
146
+ </FillButton>
147
+ </FlexBox>
148
+ </>
149
+ );
10
150
  };
package/src/index.tsx CHANGED
@@ -1,24 +1,79 @@
1
+ import { Box, IconButton } from '@codecademy/gamut';
2
+ import { FaviconIcon } from '@codecademy/gamut-icons';
3
+ import { Background, states, system } from '@codecademy/gamut-styles';
4
+ import { StyleProps } from '@codecademy/variance';
5
+ import styled from '@emotion/styled';
1
6
  import React, { useState } from 'react';
2
7
 
8
+ import { languageOption } from './consts';
3
9
  import { Editor } from './editor';
4
10
 
11
+ const editorStates = states({
12
+ isIFrame: { height: '100vh' },
13
+ });
14
+
15
+ const editorBaseStyles = system.css({
16
+ border: 1,
17
+ borderColor: 'gray-900',
18
+ display: 'flex',
19
+ flexDirection: 'column',
20
+ height: '25rem',
21
+ width: '43rem',
22
+ overflow: 'hidden',
23
+ });
24
+ export interface EditorStyleProps
25
+ extends StyleProps<typeof editorBaseStyles>,
26
+ StyleProps<typeof editorStates> {}
27
+
28
+ const EditorContainer = styled(Background)<EditorStyleProps>(
29
+ editorBaseStyles,
30
+ editorStates
31
+ );
32
+
5
33
  export interface CodeByteEditorProps {
6
34
  text: string;
35
+ language: languageOption;
36
+ hideCopyButton: boolean;
37
+ onCopy?: (text: string, language: string) => void;
38
+ isIFrame?: boolean;
39
+ snippetsBaseUrl?: string;
40
+ onTextChange?: (text: string) => void;
7
41
  }
8
42
 
9
43
  export const CodeByteEditor: React.FC<CodeByteEditorProps> = ({
10
44
  text: initialText,
45
+ language,
46
+ hideCopyButton,
47
+ onCopy,
48
+ isIFrame = false,
49
+ snippetsBaseUrl = process.env.CONTAINER_API_BASE,
50
+ onTextChange,
11
51
  }) => {
12
52
  const [text, setText] = useState<string>(initialText);
13
53
  return (
14
- <>
54
+ <EditorContainer bg="black" as="main" isIFrame={isIFrame}>
55
+ <Box borderBottom={1} borderColor="gray-900" py={4} pl={8}>
56
+ <IconButton
57
+ icon={FaviconIcon}
58
+ variant="secondary"
59
+ href="https://www.codecademy.com/"
60
+ target="_blank"
61
+ rel="noreferrer"
62
+ aria-label="visit codecademy.com"
63
+ />
64
+ </Box>
15
65
  <Editor
66
+ language={language}
16
67
  text={text}
68
+ hideCopyButton={hideCopyButton}
17
69
  onChange={(newText: string) => {
18
70
  setText(newText);
71
+ onTextChange?.(newText);
19
72
  }}
73
+ onCopy={onCopy}
74
+ snippetsBaseUrl={snippetsBaseUrl}
20
75
  />
21
- </>
76
+ </EditorContainer>
22
77
  );
23
78
  };
24
79
 
package/src/theme.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { CoreTheme } from '@codecademy/gamut-styles';
2
+
3
+ declare module '@emotion/react' {
4
+ export interface Theme extends CoreTheme {}
5
+ }
package/src/editor.d.ts DELETED
@@ -1,7 +0,0 @@
1
- import React from 'react';
2
- declare type EditorProps = {
3
- text: string;
4
- onChange: (text: string) => void;
5
- };
6
- export declare const Editor: React.FC<EditorProps>;
7
- export {};
package/src/index.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import React from 'react';
2
- export interface CodeByteEditorProps {
3
- text: string;
4
- }
5
- export declare const CodeByteEditor: React.FC<CodeByteEditorProps>;
6
- export default CodeByteEditor;