@codecademy/codebytes 0.1.0 → 0.1.1-alpha.19caf8.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 +8 -0
- package/package.json +2 -2
- package/src/consts.ts +15 -0
- package/src/drawers.tsx +128 -0
- package/src/editor.tsx +156 -4
- package/src/index.tsx +41 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.1.1-alpha.19caf8.0](https://github.com/Codecademy/client-modules/compare/@codecademy/codebytes@0.1.0...@codecademy/codebytes@0.1.1-alpha.19caf8.0) (2021-12-21)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @codecademy/codebytes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
## 0.1.0 (2021-12-17)
|
|
7
15
|
|
|
8
16
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codecademy/codebytes",
|
|
3
3
|
"description": "Codebytes Code Editor",
|
|
4
|
-
"version": "0.1.0",
|
|
4
|
+
"version": "0.1.1-alpha.19caf8.0",
|
|
5
5
|
"author": "Codecademy Engineering <dev@codecademy.com>",
|
|
6
6
|
"sideEffects": [
|
|
7
7
|
"**/*.css",
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "5df74cc4516c8894a448ede333784ca4deb371e7"
|
|
47
47
|
}
|
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;
|
package/src/drawers.tsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
${!open && hideOnClose ? 'visibility: hidden;' : ''}
|
|
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 [show, setShow] = useState<'left' | 'right' | 'both'>('both');
|
|
40
|
+
|
|
41
|
+
let buttonLabelL = 'hide code';
|
|
42
|
+
let buttonLabelR = 'hide output';
|
|
43
|
+
|
|
44
|
+
if (show === 'left') {
|
|
45
|
+
buttonLabelL = buttonLabelR = 'show output';
|
|
46
|
+
} else if (show === 'right') {
|
|
47
|
+
buttonLabelL = buttonLabelR = 'show code';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<FlexBox>
|
|
53
|
+
<Drawer
|
|
54
|
+
open={show !== 'right'}
|
|
55
|
+
alignItems="center"
|
|
56
|
+
flexWrap="nowrap"
|
|
57
|
+
textAlign="left"
|
|
58
|
+
borderRight="1"
|
|
59
|
+
borderColor="gray-900"
|
|
60
|
+
px="8"
|
|
61
|
+
>
|
|
62
|
+
<IconButton
|
|
63
|
+
icon={LeftDrawerIcon}
|
|
64
|
+
variant="secondary"
|
|
65
|
+
size="small"
|
|
66
|
+
onClick={() =>
|
|
67
|
+
setShow((state) => (state === 'both' ? 'right' : 'both'))
|
|
68
|
+
}
|
|
69
|
+
aria-label={buttonLabelL}
|
|
70
|
+
aria-controls="code-drawer"
|
|
71
|
+
aria-expanded={show !== 'right'}
|
|
72
|
+
/>
|
|
73
|
+
<DrawerLabel id="code-drawer-label">Code</DrawerLabel>
|
|
74
|
+
</Drawer>
|
|
75
|
+
<Drawer
|
|
76
|
+
open={show !== 'left'}
|
|
77
|
+
alignItems="center"
|
|
78
|
+
flexWrap="nowrap"
|
|
79
|
+
justifyContent="flex-end"
|
|
80
|
+
px="8"
|
|
81
|
+
>
|
|
82
|
+
<DrawerLabel id="output-drawer-label">Output</DrawerLabel>
|
|
83
|
+
<IconButton
|
|
84
|
+
icon={RightDrawerIcon}
|
|
85
|
+
variant="secondary"
|
|
86
|
+
size="small"
|
|
87
|
+
onClick={() =>
|
|
88
|
+
setShow((state) => (state === 'both' ? 'left' : 'both'))
|
|
89
|
+
}
|
|
90
|
+
aria-label={buttonLabelR}
|
|
91
|
+
aria-controls="output-drawer"
|
|
92
|
+
aria-expanded={show !== 'left'}
|
|
93
|
+
/>
|
|
94
|
+
</Drawer>
|
|
95
|
+
</FlexBox>
|
|
96
|
+
<FlexBox
|
|
97
|
+
flexGrow={1}
|
|
98
|
+
borderY="1"
|
|
99
|
+
borderColor="gray-900"
|
|
100
|
+
alignItems="stretch"
|
|
101
|
+
overflow="hidden"
|
|
102
|
+
>
|
|
103
|
+
<Drawer
|
|
104
|
+
hideOnClose
|
|
105
|
+
id="code-drawer"
|
|
106
|
+
aria-labelledby="code-drawer-label"
|
|
107
|
+
open={show !== 'right'}
|
|
108
|
+
flexGrow={0}
|
|
109
|
+
overflow="hidden"
|
|
110
|
+
borderColor="gray-900"
|
|
111
|
+
borderStyleRight="solid"
|
|
112
|
+
borderWidthRight="thin"
|
|
113
|
+
>
|
|
114
|
+
{leftChild}
|
|
115
|
+
</Drawer>
|
|
116
|
+
<Drawer
|
|
117
|
+
hideOnClose
|
|
118
|
+
id="output-drawer"
|
|
119
|
+
aria-labelledby="output-drawer-label"
|
|
120
|
+
open={show !== 'left'}
|
|
121
|
+
overflow="hidden"
|
|
122
|
+
>
|
|
123
|
+
{rightChild}
|
|
124
|
+
</Drawer>
|
|
125
|
+
</FlexBox>
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
};
|
package/src/editor.tsx
CHANGED
|
@@ -1,10 +1,162 @@
|
|
|
1
|
-
import
|
|
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 type { languageOption } from './consts';
|
|
14
|
+
import { Drawers } from './drawers';
|
|
15
|
+
|
|
16
|
+
const Output = styled.pre<{ hasError: boolean }>`
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 20rem;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0 1rem;
|
|
21
|
+
font-family: Monaco;
|
|
22
|
+
font-size: 0.875rem;
|
|
23
|
+
overflow: auto;
|
|
24
|
+
${({ hasError }) => `
|
|
25
|
+
color: ${hasError ? theme.colors.orange : theme.colors.white};
|
|
26
|
+
background-color: ${theme.colors['gray-900']};
|
|
27
|
+
`}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const CopyIconStyled = styled(CopyIcon)`
|
|
31
|
+
margin-right: 0.5rem;
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const DOCKER_SIGTERM = 143;
|
|
2
35
|
|
|
3
36
|
type EditorProps = {
|
|
37
|
+
hideCopyButton: boolean;
|
|
38
|
+
language: languageOption;
|
|
4
39
|
text: string;
|
|
5
|
-
|
|
40
|
+
// eslint-disable-next-line react/no-unused-prop-types
|
|
41
|
+
onChange: (text: string) => void /* Will use in upcoming pr */;
|
|
42
|
+
onCopy?: (text: string, language: string) => void;
|
|
6
43
|
};
|
|
7
44
|
|
|
8
|
-
|
|
9
|
-
|
|
45
|
+
interface Response {
|
|
46
|
+
stderr: string;
|
|
47
|
+
stdout: string;
|
|
48
|
+
exit_code: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Editor: React.FC<EditorProps> = ({
|
|
52
|
+
language,
|
|
53
|
+
text,
|
|
54
|
+
hideCopyButton,
|
|
55
|
+
onCopy,
|
|
56
|
+
}) => {
|
|
57
|
+
const [output, setOutput] = useState('');
|
|
58
|
+
const [status, setStatus] = useState<'ready' | 'waiting' | 'error'>('ready');
|
|
59
|
+
const [isCodeByteCopied, setIsCodeByteCopied] = useState(false);
|
|
60
|
+
const onCopyClick = () => {
|
|
61
|
+
if (!isCodeByteCopied) {
|
|
62
|
+
navigator.clipboard
|
|
63
|
+
.writeText(text)
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
.catch(() => console.error('Failed to copy'));
|
|
66
|
+
onCopy?.(
|
|
67
|
+
text,
|
|
68
|
+
language
|
|
69
|
+
); /* TODO: pass in onCopyBBCodeblock behavior from static sites */
|
|
70
|
+
setIsCodeByteCopied(true);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const setErrorStatusAndOutput = (message: string) => {
|
|
75
|
+
setOutput(message);
|
|
76
|
+
setStatus('error');
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleSubmit = () => {
|
|
80
|
+
if (text.trim().length === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const data = {
|
|
84
|
+
language,
|
|
85
|
+
code: text,
|
|
86
|
+
};
|
|
87
|
+
setStatus('waiting');
|
|
88
|
+
setOutput('');
|
|
89
|
+
|
|
90
|
+
const snippetsEndpoint =
|
|
91
|
+
'https://' + process.env.GATSBY_CONTAINER_API_BASE + '/snippets';
|
|
92
|
+
|
|
93
|
+
fetch(snippetsEndpoint, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
body: JSON.stringify(data),
|
|
96
|
+
headers: {
|
|
97
|
+
'x-codecademy-user-id': 'codebytes-anon-user',
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
.then((res) => res.json())
|
|
101
|
+
.then((response: Response) => {
|
|
102
|
+
if (response.stderr.length > 0) {
|
|
103
|
+
setErrorStatusAndOutput(response.stderr);
|
|
104
|
+
} else if (response.exit_code === DOCKER_SIGTERM) {
|
|
105
|
+
setErrorStatusAndOutput(
|
|
106
|
+
'Your code took too long to return a result. Double check your code for any issues and try again!'
|
|
107
|
+
);
|
|
108
|
+
} else if (response.exit_code !== 0) {
|
|
109
|
+
setErrorStatusAndOutput('An unknown error occured.');
|
|
110
|
+
} else {
|
|
111
|
+
setOutput(response.stdout);
|
|
112
|
+
setStatus('ready');
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.catch((error) => {
|
|
116
|
+
setErrorStatusAndOutput('Error: ' + error);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<Drawers
|
|
123
|
+
leftChild={<div>{text}</div>}
|
|
124
|
+
rightChild={
|
|
125
|
+
<Output hasError={status === 'error'} aria-live="polite">
|
|
126
|
+
{output}
|
|
127
|
+
</Output>
|
|
128
|
+
}
|
|
129
|
+
/>
|
|
130
|
+
<FlexBox
|
|
131
|
+
justifyContent={hideCopyButton ? 'flex-end' : 'space-between'}
|
|
132
|
+
pl="8"
|
|
133
|
+
>
|
|
134
|
+
{!hideCopyButton ? (
|
|
135
|
+
<ToolTip
|
|
136
|
+
id="codebyte-copied"
|
|
137
|
+
alignment="top-right"
|
|
138
|
+
mode="dark"
|
|
139
|
+
target={
|
|
140
|
+
<TextButton
|
|
141
|
+
variant="secondary"
|
|
142
|
+
onClick={onCopyClick}
|
|
143
|
+
onBlur={() => setIsCodeByteCopied(false)}
|
|
144
|
+
>
|
|
145
|
+
<CopyIconStyled aria-hidden="true" /> Copy Codebyte
|
|
146
|
+
</TextButton>
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
{isCodeByteCopied ? (
|
|
150
|
+
<span role="alert">Copied!</span>
|
|
151
|
+
) : (
|
|
152
|
+
<span>Copy to your clipboard</span>
|
|
153
|
+
)}
|
|
154
|
+
</ToolTip>
|
|
155
|
+
) : null}
|
|
156
|
+
<FillButton onClick={handleSubmit}>
|
|
157
|
+
{status === 'waiting' ? <Spinner /> : 'Run'}
|
|
158
|
+
</FillButton>
|
|
159
|
+
</FlexBox>
|
|
160
|
+
</>
|
|
161
|
+
);
|
|
10
162
|
};
|
package/src/index.tsx
CHANGED
|
@@ -1,23 +1,58 @@
|
|
|
1
|
+
import { Box, IconButton } from '@codecademy/gamut';
|
|
2
|
+
import { FaviconIcon } from '@codecademy/gamut-icons';
|
|
3
|
+
import { Background, system } from '@codecademy/gamut-styles';
|
|
4
|
+
import styled from '@emotion/styled';
|
|
1
5
|
import React, { useState } from 'react';
|
|
2
6
|
|
|
7
|
+
import { languageOption } from './consts';
|
|
3
8
|
import { Editor } from './editor';
|
|
4
9
|
|
|
5
10
|
export interface CodeByteEditorProps {
|
|
6
11
|
text: string;
|
|
12
|
+
language: languageOption;
|
|
13
|
+
hideCopyButton: boolean;
|
|
7
14
|
}
|
|
8
15
|
|
|
16
|
+
const EditorContainer = styled(Background)(
|
|
17
|
+
system.css({
|
|
18
|
+
border: '1',
|
|
19
|
+
borderColor: 'gray-900',
|
|
20
|
+
display: 'flex',
|
|
21
|
+
flexDirection: 'column',
|
|
22
|
+
height: '400px',
|
|
23
|
+
width: '688px',
|
|
24
|
+
overflow: 'hidden',
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
9
28
|
export const CodeByteEditor: React.FC<CodeByteEditorProps> = ({
|
|
10
29
|
text: initialText,
|
|
30
|
+
language,
|
|
31
|
+
hideCopyButton,
|
|
11
32
|
}) => {
|
|
12
33
|
const [text, setText] = useState<string>(initialText);
|
|
13
34
|
return (
|
|
14
35
|
<>
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
<EditorContainer bg="black" as="main">
|
|
37
|
+
<Box borderBottom="1" borderColor="gray-900" py="4" pl="8">
|
|
38
|
+
<IconButton
|
|
39
|
+
icon={FaviconIcon}
|
|
40
|
+
variant="secondary"
|
|
41
|
+
href="https://www.codecademy.com/"
|
|
42
|
+
target="_blank"
|
|
43
|
+
rel="noreferrer"
|
|
44
|
+
aria-label="visit codecademy.com"
|
|
45
|
+
/>
|
|
46
|
+
</Box>
|
|
47
|
+
<Editor
|
|
48
|
+
text={text}
|
|
49
|
+
onChange={(newText: string) => {
|
|
50
|
+
setText(newText);
|
|
51
|
+
}}
|
|
52
|
+
hideCopyButton={hideCopyButton}
|
|
53
|
+
language={language}
|
|
54
|
+
/>
|
|
55
|
+
</EditorContainer>
|
|
21
56
|
</>
|
|
22
57
|
);
|
|
23
58
|
};
|