diff --git a/.gitignore b/.gitignore index 1b1ce6991..f1c498a5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ .DS_Store -# Env -.env.* - # Logs logs *.log diff --git a/packages/codebytes/package.json b/packages/codebytes/package.json index d942d8af5..a576bd80d 100644 --- a/packages/codebytes/package.json +++ b/packages/codebytes/package.json @@ -25,7 +25,10 @@ "@codecademy/gamut-styles": "*", "@codecademy/variance": "*", "@emotion/react": "^11.4.0", - "@emotion/styled": "^11.3.0" + "@emotion/styled": "^11.3.0", + "@monaco-editor/react": "4.3.1", + "react-resize-observer": "1.1.1", + "monaco-editor": ">= 0.25.0 < 1" }, "scripts": { "verify": "tsc --noEmit", @@ -39,7 +42,8 @@ "devDependencies": { "@emotion/jest": "^11.3.0", "@testing-library/dom": "^7.31.2", - "@testing-library/react": "^11.0.4" + "@testing-library/react": "^11.0.4", + "@types/loadable__component": "^5.13.2" }, "publishConfig": { "access": "public" diff --git a/packages/codebytes/src/MonacoEditor/colorsDark.ts b/packages/codebytes/src/MonacoEditor/colorsDark.ts new file mode 100644 index 000000000..c49d5993b --- /dev/null +++ b/packages/codebytes/src/MonacoEditor/colorsDark.ts @@ -0,0 +1,54 @@ +// DO NOT CHANGE ANYTHING HERE +// This file is part of the Codebytes MVP and only includes basic configuration around theming for the SimpleMonacoEditor component +// We are working on a monaco package in client-modules that has more configuration around themes and languages +// Monaco as a shared package RFC https://www.notion.so/codecademy/Monaco-editor-as-a-shared-package-1f4484db165b4abc8394c3556451ef6a + +import { colors, editorColors } from '@codecademy/gamut-styles'; + +const darkTheme = { + blue: editorColors.blue, + deepPurple: editorColors.deepPurple, + gray: editorColors.gray, + green: editorColors.green, + orange: editorColors.orange, + purple: editorColors.purple, + red: editorColors.red, + white: colors.white, + yellow: editorColors.yellow, +}; + +export const syntax = { + attribute: darkTheme.green, + annotation: darkTheme.red, + atom: darkTheme.deepPurple, + basic: darkTheme.white, + comment: darkTheme.gray, + constant: darkTheme.orange, + decoration: darkTheme.red, + invalid: darkTheme.red, + key: darkTheme.blue, + keyword: darkTheme.purple, + number: darkTheme.red, + operator: darkTheme.red, + predefined: darkTheme.white, + property: darkTheme.red, + regexp: darkTheme.green, + string: darkTheme.yellow, + tag: darkTheme.red, + text: darkTheme.orange, + value: darkTheme.yellow, + variable: darkTheme.green, +}; + +export const ui = { + background: '#211E2F', + text: darkTheme.white, + indent: { + active: '#393b41', + inactive: '#494b51', + }, +}; + +export type SyntaxColors = typeof syntax; + +export type UIColors = typeof ui; diff --git a/packages/codebytes/src/MonacoEditor/index.tsx b/packages/codebytes/src/MonacoEditor/index.tsx new file mode 100644 index 000000000..018ab8686 --- /dev/null +++ b/packages/codebytes/src/MonacoEditor/index.tsx @@ -0,0 +1,53 @@ +// DO NOT CHANGE ANYTHING HERE +// This component is part of the Codebytes MVP and only includes basic configuration around theming +// We are working on a monaco package in client-modules that has more configuration around themes and languages +// Monaco as a shared package RFC https://www.notion.so/codecademy/Monaco-editor-as-a-shared-package-1f4484db165b4abc8394c3556451ef6a + +import ReactMonacoEditor, { OnMount } from '@monaco-editor/react'; +import { editor } from 'monaco-editor/esm/vs/editor/editor.api'; +import React, { useCallback, useRef } from 'react'; +import ResizeObserver from 'react-resize-observer'; + +import { dark } from './theme'; +import { Monaco } from './types'; + +export type SimpleMonacoEditorProps = { + value: string; + language: string; + onChange?: (value: string) => void; +}; + +type ThemedEditor = Parameters[0]; + +export const SimpleMonacoEditor: React.FC = ({ + value, + language, + onChange, +}) => { + const editorRef = useRef(null); + const editorOnMount = useCallback((editor: ThemedEditor, monaco: Monaco) => { + editorRef.current = editor; + monaco.editor.defineTheme('dark', dark); + monaco.editor.setTheme('dark'); + }, []); + return ( + <> + { + editorRef.current?.layout({ + height, + width, + }); + }} + /> + + + ); +}; diff --git a/packages/codebytes/src/MonacoEditor/theme.ts b/packages/codebytes/src/MonacoEditor/theme.ts new file mode 100644 index 000000000..70d3ac779 --- /dev/null +++ b/packages/codebytes/src/MonacoEditor/theme.ts @@ -0,0 +1,64 @@ +// DO NOT CHANGE ANYTHING HERE +// This file is part of the Codebytes MVP and only includes basic configuration around theming for the SimpleMonacoEditor component +// We are working on a monaco package in client-modules that has more configuration around themes and languages +// Monaco as a shared package RFC https://www.notion.so/codecademy/Monaco-editor-as-a-shared-package-1f4484db165b4abc8394c3556451ef6a + +import type * as monaco from 'monaco-editor'; + +import * as darkColors from './colorsDark'; + +const c = (color: string) => color.substr(1); + +const theme = ({ + ui, + syntax, +}: { + ui: darkColors.UIColors; + syntax: darkColors.SyntaxColors; +}): monaco.editor.IStandaloneThemeData => ({ + base: 'vs-dark', + inherit: true, + rules: [ + // Base + { token: '', foreground: c(syntax.basic) }, + { token: 'regexp', foreground: c(syntax.regexp) }, + { token: 'annotation', foreground: c(syntax.annotation) }, + { token: 'type', foreground: c(syntax.annotation) }, + { token: 'doctype', foreground: c(syntax.comment) }, + { token: 'delimiter', foreground: c(syntax.decoration) }, + { token: 'invalid', foreground: c(syntax.invalid) }, + { token: 'emphasis', fontStyle: 'italic' }, + { token: 'strong', fontStyle: 'bold' }, + { token: 'variable', foreground: c(syntax.variable) }, + { token: 'variable.predefined', foreground: c(syntax.variable) }, + { token: 'constant', foreground: c(syntax.constant) }, + { token: 'comment', foreground: c(syntax.comment) }, + { token: 'number', foreground: c(syntax.number) }, + { token: 'number.hex', foreground: c(syntax.number) }, + { token: 'keyword.directive', foreground: c(syntax.comment) }, + { token: 'include', foreground: c(syntax.comment) }, + { token: 'key', foreground: c(syntax.property) }, + { token: 'attribute.name', foreground: c(syntax.attribute) }, + { token: 'attribute.name-numeric', foreground: c(syntax.string) }, + { token: 'attribute.value', foreground: c(syntax.property) }, + { token: 'attribute.value.number', foreground: c(syntax.number) }, + { token: 'string', foreground: c(syntax.string) }, + { token: 'string.yaml', foreground: c(syntax.string) }, + { token: 'tag', foreground: c(syntax.tag) }, + { token: 'tag.id.jade', foreground: c(syntax.tag) }, + { token: 'tag.class.jade', foreground: c(syntax.tag) }, + { token: 'metatag', foreground: c(syntax.comment) }, + { token: 'attribute.value.unit', foreground: c(syntax.string) }, + { token: 'keyword', foreground: c(syntax.keyword) }, + { token: 'keyword.flow', foreground: c(syntax.keyword) }, + ], + colors: { + 'editor.background': ui.background, + 'editor.foreground': ui.text, + 'editorIndentGuide.background': ui.indent.inactive, + 'editorIndentGuide.activeBackground': ui.indent.active, + 'editorWhitespace.foreground': syntax.comment, + }, +}); + +export const dark = theme(darkColors); diff --git a/packages/codebytes/src/MonacoEditor/types.ts b/packages/codebytes/src/MonacoEditor/types.ts new file mode 100644 index 000000000..60677b961 --- /dev/null +++ b/packages/codebytes/src/MonacoEditor/types.ts @@ -0,0 +1 @@ +export type Monaco = typeof import('monaco-editor'); diff --git a/packages/codebytes/src/consts.ts b/packages/codebytes/src/consts.ts index 18064b9c4..9115e7a8c 100644 --- a/packages/codebytes/src/consts.ts +++ b/packages/codebytes/src/consts.ts @@ -13,3 +13,52 @@ export const languageOptions = { }; export type languageOption = keyof typeof languageOptions; + +export const validLanguages = Object.keys(languageOptions).filter( + (option) => !!option +) as languageOption[]; + +const cpp = `#include +int main() { + std::cout << "Hello world!"; + return 0; +}`; + +const csharp = `namespace HelloWorld { + class Hello { + static void Main(string[] args) { + System.Console.WriteLine("Hello world!"); + } + } +}`; + +const golang = `package main +import "fmt" +func main() { + fmt.Println("Hello world!") +}`; + +const javascript = "console.log('Hello world!');"; + +const php = ``; + +const python = "print('Hello world!')"; + +const ruby = 'puts "Hello world!"'; + +const scheme = `(begin + (display "Hello world!") + (newline))`; + +export const helloWorld: { [key in languageOption]?: string } = { + cpp, + csharp, + golang, + javascript, + php, + python, + ruby, + scheme, +}; diff --git a/packages/codebytes/src/editor.tsx b/packages/codebytes/src/editor.tsx index 49529fba0..ee070a88b 100644 --- a/packages/codebytes/src/editor.tsx +++ b/packages/codebytes/src/editor.tsx @@ -6,13 +6,14 @@ import { ToolTip, } from '@codecademy/gamut'; import { CopyIcon } from '@codecademy/gamut-icons'; -import { theme } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import React, { useState } from 'react'; import { postSnippet } from './api'; import type { languageOption } from './consts'; import { Drawers } from './drawers'; +import { SimpleMonacoEditor } from './MonacoEditor'; +import { CodebytesChangeHandlerMap } from './types'; const Output = styled.pre<{ hasError: boolean }>` width: 100%; @@ -22,9 +23,9 @@ const Output = styled.pre<{ hasError: boolean }>` font-family: Monaco; font-size: 0.875rem; overflow: auto; - ${({ hasError }) => ` + ${({ hasError, theme }) => ` color: ${hasError ? theme.colors.orange : theme.colors.white}; - background-color: ${theme.colors['gray-900']}; + background-color: ${theme.colors['navy-900']}; `} `; @@ -38,11 +39,9 @@ type EditorProps = { hideCopyButton: boolean; language: languageOption; text: string; - // eslint-disable-next-line react/no-unused-prop-types - onChange: ( - text: string - ) => void /* TODO: Add onChange behavior in DISC-353 */; - onCopy?: (text: string, language: string) => void; + + onChange: (text: string) => void; + on?: Pick; snippetsBaseUrl?: string; }; @@ -50,7 +49,8 @@ export const Editor: React.FC = ({ language, text, hideCopyButton, - onCopy, + on, + onChange, snippetsBaseUrl, }) => { const [output, setOutput] = useState(''); @@ -62,7 +62,7 @@ export const Editor: React.FC = ({ .writeText(text) // eslint-disable-next-line no-console .catch(() => console.error('Failed to copy')); - onCopy?.(text, language); + on?.copy?.(text, language); setIsCodeByteCopied(true); } }; @@ -82,6 +82,7 @@ export const Editor: React.FC = ({ }; setStatus('waiting'); setOutput(''); + on?.run?.(); try { const response = await postSnippet(data, snippetsBaseUrl); @@ -105,7 +106,13 @@ export const Editor: React.FC = ({ return ( <> {text}} + leftChild={ + + } rightChild={ {output} diff --git a/packages/codebytes/src/index.tsx b/packages/codebytes/src/index.tsx index 3168266ff..40ad71781 100644 --- a/packages/codebytes/src/index.tsx +++ b/packages/codebytes/src/index.tsx @@ -5,8 +5,10 @@ import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; import React, { useState } from 'react'; -import { languageOption } from './consts'; +import { helloWorld, languageOption } from './consts'; import { Editor } from './editor'; +import { LanguageSelection } from './languageSelection'; +import { CodeByteEditorProps } from './types'; const editorStates = states({ isIFrame: { height: '100vh' }, @@ -30,26 +32,16 @@ const EditorContainer = styled(Background)( editorStates ); -export interface CodeByteEditorProps { - text: string; - language: languageOption; - hideCopyButton: boolean; - onCopy?: (text: string, language: string) => void; - isIFrame?: boolean; - snippetsBaseUrl?: string /* TODO in DISC-353: Determine best way to host and include snippets endpoint for both staging and production in both the monolith and next.js repo. */; - onTextChange?: (text: string) => void; -} - export const CodeByteEditor: React.FC = ({ text: initialText, - language, + language: initialLanguage, hideCopyButton, - onCopy, isIFrame = false, - snippetsBaseUrl = '', - onTextChange, + on, + snippetsBaseUrl = process.env.CONTAINER_API_BASE, }) => { const [text, setText] = useState(initialText); + const [language, setLanguage] = useState(initialLanguage); return ( @@ -60,19 +52,31 @@ export const CodeByteEditor: React.FC = ({ target="_blank" rel="noreferrer" aria-label="visit codecademy.com" + onClick={() => on?.logoClick?.()} /> - { - setText(newText); - onTextChange?.(newText); - }} - onCopy={onCopy} - snippetsBaseUrl={snippetsBaseUrl} - /> + {language ? ( + { + setText(newText); + on?.edit?.(newText, language); + }} + on={on} + snippetsBaseUrl={snippetsBaseUrl} + /> + ) : ( + { + const newText: string = text || helloWorld[newLanguage] || ''; + setLanguage(newLanguage); + setText(newText); + on?.languageChange?.(newText, newLanguage); + }} + /> + )} ); }; diff --git a/packages/codebytes/src/languageSelection.tsx b/packages/codebytes/src/languageSelection.tsx new file mode 100644 index 000000000..7b6466390 --- /dev/null +++ b/packages/codebytes/src/languageSelection.tsx @@ -0,0 +1,45 @@ +import { Select } from '@codecademy/gamut'; +import { Background } from '@codecademy/gamut-styles'; +import styled from '@emotion/styled'; +import React from 'react'; + +import type { languageOption } from './consts'; +import { languageOptions } from './consts'; + +const StyledSelect = styled(Select)` + margin-top: 1rem; + color: ${({ theme }) => theme.colors.text}; + select { + color: ${({ theme }) => theme.colors.text}; + background-color: ${({ theme }) => theme.colors.black}; + + &:hover, + &:active, + &:focus { + border-color: ${({ theme }) => theme.colors.primary}; + } + &:focus { + box-shadow: inset 0 0 0 1px ${({ theme }) => theme.colors.primary}; + } + } +`; + +export type LanguageSelectionProps = { + onChange: (newLanguage: languageOption) => void; +}; + +export const LanguageSelection: React.FC = ({ + onChange, +}) => { + return ( + + Which language do you want to code in? + onChange(e.target.value as languageOption)} + /> + + ); +}; diff --git a/packages/codebytes/src/theme.d.ts b/packages/codebytes/src/theme.d.ts index e2a52671b..dc4ce8c54 100644 --- a/packages/codebytes/src/theme.d.ts +++ b/packages/codebytes/src/theme.d.ts @@ -1,5 +1,4 @@ import { CoreTheme } from '@codecademy/gamut-styles'; - declare module '@emotion/react' { export interface Theme extends CoreTheme {} } diff --git a/packages/codebytes/src/types.ts b/packages/codebytes/src/types.ts new file mode 100644 index 000000000..efb7735fa --- /dev/null +++ b/packages/codebytes/src/types.ts @@ -0,0 +1,20 @@ +import { languageOption } from './consts'; + +type CodebytesChangeHandler = (text: string, language: languageOption) => void; + +export type CodebytesChangeHandlerMap = { + logoClick?: () => void; + edit?: CodebytesChangeHandler; + languageChange?: CodebytesChangeHandler; + copy?: CodebytesChangeHandler; + run?: () => void; +}; + +export interface CodeByteEditorProps { + text: string; + language: languageOption; + hideCopyButton: boolean; + isIFrame?: boolean; + snippetsBaseUrl?: string /* TODO in DISC-353: Determine best way to host and include snippets endpoint for both staging and production in both the monolith and next.js repo. */; + on?: CodebytesChangeHandlerMap; +} diff --git a/packages/styleguide/.env.local b/packages/styleguide/.env.local new file mode 100644 index 000000000..e82e1aa11 --- /dev/null +++ b/packages/styleguide/.env.local @@ -0,0 +1 @@ +CONTAINER_API_BASE=propeller.cc-le-cf.com diff --git a/packages/styleguide/.storybook/main.ts b/packages/styleguide/.storybook/main.ts index 6f3b4c083..f709a7d10 100644 --- a/packages/styleguide/.storybook/main.ts +++ b/packages/styleguide/.storybook/main.ts @@ -30,9 +30,11 @@ module.exports = { }, webpackFinal: (config: any) => { - config.module.rules = config.module.rules.concat( - configs.css().module.rules - ); + const commonRules = configs.css().module.rules; /* Codecademy configs */ + config.module.rules = config.module.rules.concat([ + { ...commonRules[0], include: [/node_modules\/@codecademy/] }, + commonRules[1], + ]); config.resolve = { ...config.resolve, diff --git a/packages/styleguide/package.json b/packages/styleguide/package.json index 3c22c6eb4..1624d5296 100644 --- a/packages/styleguide/package.json +++ b/packages/styleguide/package.json @@ -19,6 +19,7 @@ "@codecademy/gamut": "^42.1.1", "@codecademy/variance": "*", "@codecademy/webpack-config": "^6.0.0", + "@codecademy/variance": "*", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.4.0", "@emotion/styled": "^11.3.0", diff --git a/packages/styleguide/stories/CodeByteEditor.stories.mdx b/packages/styleguide/stories/CodeByteEditor.stories.mdx index 90b886b79..f15ac6cfa 100644 --- a/packages/styleguide/stories/CodeByteEditor.stories.mdx +++ b/packages/styleguide/stories/CodeByteEditor.stories.mdx @@ -1,5 +1,4 @@ import { CodeByteEditor } from '@codecademy/codebytes/src'; -import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; import { AppWrapper } from '@codecademy/gamut'; import { createEmotionCache, @@ -7,6 +6,7 @@ import { theme, } from '@codecademy/gamut-styles'; import { AssetProvider } from '@codecademy/gamut-styles/dist/AssetProvider'; +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; + {(args) => ( + + + + + + + )} + + diff --git a/yarn.lock b/yarn.lock index 85c7f2112..add625b2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3443,6 +3443,21 @@ resolved "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== +"@monaco-editor/loader@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.2.0.tgz#373fad69973384624e3d9b60eefd786461a76acd" + integrity sha512-cJVCG/T/KxXgzYnjKqyAgsKDbH9mGLjcXxN6AmwumBwa2rVFkwvGcUj1RJtD0ko4XqLqJxwqsN/Z/KURB5f1OQ== + dependencies: + state-local "^1.0.6" + +"@monaco-editor/react@4.3.1": + version "4.3.1" + resolved "https://registry.npmjs.org/@monaco-editor/react/-/react-4.3.1.tgz#d65bcbf174c39b6d4e7fec43d0cddda82b70a12a" + integrity sha512-f+0BK1PP/W5I50hHHmwf11+Ea92E5H1VZXs+wvKplWUWOfyMa1VVwqkJrXjRvbcqHL+XdIGYWhWNdi4McEvnZg== + dependencies: + "@monaco-editor/loader" "^1.2.0" + prop-types "^15.7.2" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -4831,6 +4846,13 @@ resolved "https://registry.npmjs.org/@types/konami-code-js/-/konami-code-js-0.8.0.tgz#dd7b3bd23266d0d1c6bfcad870232b1a3788a883" integrity sha512-7fhuggz5XuuQJxWhVL/5i3oQEQ80jjp3yvwaWlwNjFbM40BELnzV4qKeyPDLtjsHC9NG6E7XT22epH9eDZC3zg== +"@types/loadable__component@^5.13.2": + version "5.13.4" + resolved "https://registry.npmjs.org/@types/loadable__component/-/loadable__component-5.13.4.tgz#a4646b2406b1283efac1a9d9485824a905b33d4a" + integrity sha512-YhoCCxyuvP2XeZNbHbi8Wb9EMaUJuA2VGHxJffcQYrJKIKSkymJrhbzsf9y4zpTmr5pExAAEh5hbF628PAZ8Dg== + dependencies: + "@types/react" "*" + "@types/lodash@4.14.168": version "4.14.168" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" @@ -13355,6 +13377,11 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +"monaco-editor@>= 0.25.0 < 1": + version "0.31.1" + resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.31.1.tgz#67f597b3e45679d1f551237e12a3a42c4438b97b" + integrity sha512-FYPwxGZAeP6mRRyrr5XTGHD9gRXVjy7GUzF4IPChnyt3fS5WrNxIkS8DNujWf6EQy0Zlzpxw8oTVE+mWI2/D1Q== + moo@^0.5.0: version "0.5.1" resolved "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" @@ -15608,6 +15635,11 @@ react-remove-scroll@^2.4.1: use-callback-ref "^1.2.3" use-sidecar "^1.0.1" +react-resize-observer@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/react-resize-observer/-/react-resize-observer-1.1.1.tgz#641dfa2e0f4bd2549a8ab4bbbaf43b68f3dcaf76" + integrity sha512-3R+90Hou90Mr3wJYc+unsySC8Pn91V4nmjO32NKvUvjphRUbq9HisyLg7bDyGBE7xlMrrM6Fax7iNQaFdc/FYA== + react-select@^4.3.0: version "4.3.1" resolved "https://registry.npmjs.org/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81" @@ -17039,6 +17071,11 @@ stacktrace-js@^2.0.2: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" +state-local@^1.0.6: + version "1.0.7" + resolved "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5" + integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w== + state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"