├── .babelrc ├── .env.example ├── .eslintignore ├── .eslintrc ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── existing-feature-enhancement.yml │ ├── feature-request.yml │ └── found-a-bug.yml ├── PULL_REQUEST_TEMPLATE.md ├── config.yml └── workflows │ ├── deploy-staging.yml │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .storybook ├── main.js ├── preview-head.html └── preview.js ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── client ├── __mocks__ │ ├── fileMock.js │ ├── i18n.js │ └── styleMock.js ├── browserHistory.js ├── common │ ├── Button.jsx │ ├── Button.stories.jsx │ ├── ButtonOrLink.jsx │ ├── ButtonOrLink.test.jsx │ ├── icons.jsx │ └── icons.stories.jsx ├── components │ ├── AddRemoveButton.jsx │ ├── Dropdown.jsx │ ├── Nav │ │ ├── NavBar.jsx │ │ ├── NavDropdownMenu.jsx │ │ ├── NavMenuItem.jsx │ │ └── contexts.jsx │ ├── NavBasic.jsx │ ├── OverlayManager.jsx │ ├── PreviewNav.jsx │ ├── RootPage.jsx │ ├── createRedirectWithUsername.jsx │ ├── mobile │ │ ├── ActionStrip.jsx │ │ ├── Explorer.jsx │ │ ├── FloatingNav.jsx │ │ ├── Footer.jsx │ │ ├── Header.jsx │ │ ├── IDEWrapper.jsx │ │ ├── IconButton.jsx │ │ ├── MobileScreen.jsx │ │ ├── PreferencePicker.jsx │ │ ├── Sidebar.jsx │ │ ├── Tab.jsx │ │ └── TabSwitcher.jsx │ └── useAsModal.jsx ├── constants.js ├── i18n-test.js ├── i18n.js ├── images │ ├── account.svg │ ├── add.svg │ ├── check.svg │ ├── check_encircled.svg │ ├── circle-folder.svg │ ├── circle-info.svg │ ├── circle-terminal.svg │ ├── close.svg │ ├── code.svg │ ├── console-command-contrast.svg │ ├── console-command-dark.svg │ ├── console-command-light.svg │ ├── console-debug-contrast.svg │ ├── console-debug-dark.svg │ ├── console-debug-light.svg │ ├── console-error-contrast.svg │ ├── console-error-dark.svg │ ├── console-error-light.svg │ ├── console-info-contrast.svg │ ├── console-info-dark.svg │ ├── console-info-light.svg │ ├── console-result-contrast.svg │ ├── console-result-dark.svg │ ├── console-result-light.svg │ ├── console-warn-contrast.svg │ ├── console-warn-dark.svg │ ├── console-warn-light.svg │ ├── cross.svg │ ├── down-arrow-white.svg │ ├── down-arrow.svg │ ├── down-filled-triangle.svg │ ├── editor.svg │ ├── exit.svg │ ├── file.svg │ ├── filter.svg │ ├── folder-padded.svg │ ├── folder.svg │ ├── github.svg │ ├── google.svg │ ├── help.svg │ ├── information.svg │ ├── left-arrow.svg │ ├── magnifyingglass.svg │ ├── minus.svg │ ├── more.svg │ ├── p5-asterisk.svg │ ├── p5js-logo-small.svg │ ├── p5js-logo.svg │ ├── p5js-square-logo.png │ ├── p5js-square-logo.svg │ ├── pencil.svg │ ├── play.svg │ ├── plus-icon.svg │ ├── plus.svg │ ├── preferences.svg │ ├── right-arrow-white.svg │ ├── right-arrow.svg │ ├── save.svg │ ├── share.svg │ ├── sort-arrow-down.svg │ ├── sort-arrow-up.svg │ ├── stop.svg │ ├── terminal.svg │ ├── trash-can.svg │ ├── triangle-arrow-down-white.svg │ ├── triangle-arrow-down.svg │ ├── triangle-arrow-left.svg │ ├── triangle-arrow-right-white.svg │ ├── triangle-arrow-right.svg │ ├── unsaved-changes-dot.svg │ └── up-arrow.svg ├── index.integration.test.jsx ├── index.jsx ├── index.stories.mdx ├── jest.setup.js ├── modules │ ├── App │ │ ├── App.jsx │ │ └── components │ │ │ ├── DevTools.jsx │ │ │ ├── Overlay.jsx │ │ │ ├── ThemeProvider.jsx │ │ │ └── loader.jsx │ ├── IDE │ │ ├── actions │ │ │ ├── assets.js │ │ │ ├── collections.js │ │ │ ├── console.js │ │ │ ├── editorAccessibility.js │ │ │ ├── files.js │ │ │ ├── ide.js │ │ │ ├── loader.js │ │ │ ├── preferences.js │ │ │ ├── project.js │ │ │ ├── projects.js │ │ │ ├── projects.unit.test.js │ │ │ ├── sorting.js │ │ │ ├── toast.js │ │ │ └── uploader.js │ │ ├── components │ │ │ ├── About.jsx │ │ │ ├── AddToCollectionList.jsx │ │ │ ├── AddToCollectionSketchList.jsx │ │ │ ├── AssetList.jsx │ │ │ ├── AssetPreview.jsx │ │ │ ├── AssetSize.jsx │ │ │ ├── CollectionList │ │ │ │ ├── CollectionList.jsx │ │ │ │ ├── CollectionListRow.jsx │ │ │ │ └── index.js │ │ │ ├── Console.jsx │ │ │ ├── ConsoleInput.jsx │ │ │ ├── CopyableInput.jsx │ │ │ ├── EditableInput.jsx │ │ │ ├── Editor │ │ │ │ ├── Editor.unit.test.jsx │ │ │ │ ├── MobileEditor.jsx │ │ │ │ └── index.jsx │ │ │ ├── EditorAccessibility.jsx │ │ │ ├── ErrorModal.jsx │ │ │ ├── ErrorModal.stories.jsx │ │ │ ├── ErrorModal.unit.test.jsx │ │ │ ├── Feedback.jsx │ │ │ ├── FileNode.jsx │ │ │ ├── FileNode.stories.jsx │ │ │ ├── FileNode.unit.test.jsx │ │ │ ├── FileUploader.jsx │ │ │ ├── FloatingActionButton.jsx │ │ │ ├── Header │ │ │ │ ├── MobileNav.jsx │ │ │ │ ├── Nav.jsx │ │ │ │ ├── Nav.unit.test.jsx │ │ │ │ ├── ProjectName.jsx │ │ │ │ ├── Toolbar.jsx │ │ │ │ ├── Toolbar.unit.test.jsx │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── Nav.unit.test.jsx.snap │ │ │ │ └── index.jsx │ │ │ ├── IDEKeyHandlers.jsx │ │ │ ├── IDEOverlays.jsx │ │ │ ├── KeyboardShortcutModal.jsx │ │ │ ├── Modal.jsx │ │ │ ├── Modal.stories.jsx │ │ │ ├── Modal.unit.test.jsx │ │ │ ├── NewFileForm.jsx │ │ │ ├── NewFileModal.jsx │ │ │ ├── NewFolderForm.jsx │ │ │ ├── NewFolderModal.jsx │ │ │ ├── Preferences │ │ │ │ ├── PreferenceCreators.jsx │ │ │ │ ├── Preferences.unit.test.jsx │ │ │ │ └── index.jsx │ │ │ ├── PreviewFrame.jsx │ │ │ ├── QuickAddList │ │ │ │ ├── Icons.jsx │ │ │ │ ├── QuickAddList.jsx │ │ │ │ └── index.js │ │ │ ├── Searchbar │ │ │ │ ├── Collection.jsx │ │ │ │ ├── Searchbar.jsx │ │ │ │ ├── Sketch.jsx │ │ │ │ └── index.js │ │ │ ├── ShareModal.jsx │ │ │ ├── Sidebar.jsx │ │ │ ├── SketchList.jsx │ │ │ ├── SketchList.unit.test.jsx │ │ │ ├── Timer.jsx │ │ │ ├── Toast.jsx │ │ │ ├── UnsavedChangesIndicator.jsx │ │ │ ├── UploadFileModal.jsx │ │ │ ├── __snapshots__ │ │ │ │ └── SketchList.unit.test.jsx.snap │ │ │ └── show-hint.js │ │ ├── hooks │ │ │ ├── custom-hooks.js │ │ │ ├── index.js │ │ │ ├── useHandleMessageEvent.js │ │ │ ├── useInterval.js │ │ │ ├── useKeyDownHandlers.js │ │ │ ├── useSketchActions.js │ │ │ └── useWhatPage.js │ │ ├── pages │ │ │ ├── FullView.jsx │ │ │ ├── IDEView.jsx │ │ │ └── MobileIDEView.jsx │ │ ├── reducers │ │ │ ├── assets.js │ │ │ ├── collections.js │ │ │ ├── console.js │ │ │ ├── editorAccessibility.js │ │ │ ├── files.js │ │ │ ├── ide.js │ │ │ ├── loading.js │ │ │ ├── preferences.js │ │ │ ├── project.js │ │ │ ├── projects.js │ │ │ ├── search.js │ │ │ ├── sorting.js │ │ │ └── toast.js │ │ └── selectors │ │ │ ├── collections.js │ │ │ ├── files.js │ │ │ ├── project.js │ │ │ ├── projects.js │ │ │ └── users.js │ ├── Legal │ │ ├── components │ │ │ └── PolicyContainer.jsx │ │ └── pages │ │ │ ├── CodeOfConduct.jsx │ │ │ ├── Legal.jsx │ │ │ ├── PrivacyPolicy.jsx │ │ │ └── TermsOfUse.jsx │ ├── Mobile │ │ ├── MobileDashboardView.jsx │ │ ├── MobilePreferences.jsx │ │ ├── MobileSketchView.jsx │ │ └── MobileViewContent.jsx │ ├── Preview │ │ ├── EmbedFrame.jsx │ │ ├── filesReducer.js │ │ └── previewIndex.jsx │ └── User │ │ ├── actions.js │ │ ├── components │ │ ├── APIKeyForm.jsx │ │ ├── APIKeyList.jsx │ │ ├── AccountForm.jsx │ │ ├── Collection.jsx │ │ ├── CollectionCreate.jsx │ │ ├── CookieConsent.jsx │ │ ├── DashboardTabSwitcher.jsx │ │ ├── LoginForm.jsx │ │ ├── NewPasswordForm.jsx │ │ ├── Notification.jsx │ │ ├── ResetPasswordForm.jsx │ │ ├── ResponsiveForm.jsx │ │ ├── SignupForm.jsx │ │ ├── SocialAuthButton.jsx │ │ └── SocialAuthButton.stories.jsx │ │ ├── pages │ │ ├── AccountView.jsx │ │ ├── CollectionView.jsx │ │ ├── DashboardView.jsx │ │ ├── EmailVerificationView.jsx │ │ ├── LoginView.jsx │ │ ├── NewPasswordView.jsx │ │ ├── ResetPasswordView.jsx │ │ └── SignupView.jsx │ │ └── reducers.js ├── persistState.js ├── reducers.js ├── routes.jsx ├── sounds │ └── audioAlert.mp3 ├── store.js ├── styles │ ├── .gitignore │ ├── abstracts │ │ ├── _functions.scss │ │ ├── _mixins.scss │ │ ├── _placeholders.scss │ │ └── _variables.scss │ ├── base │ │ ├── _base.scss │ │ └── _reset.scss │ ├── components │ │ ├── _about.scss │ │ ├── _account.scss │ │ ├── _api-key.scss │ │ ├── _asset-list.scss │ │ ├── _asset-size.scss │ │ ├── _collection-create.scss │ │ ├── _collection.scss │ │ ├── _console-input.scss │ │ ├── _console.scss │ │ ├── _copyable-input.scss │ │ ├── _dashboard-header.scss │ │ ├── _editable-input.scss │ │ ├── _editor.scss │ │ ├── _error-modal.scss │ │ ├── _feedback.scss │ │ ├── _form-container.scss │ │ ├── _forms.scss │ │ ├── _hints.scss │ │ ├── _keyboard-shortcuts.scss │ │ ├── _loader.scss │ │ ├── _modal.scss │ │ ├── _nav.scss │ │ ├── _new-password.scss │ │ ├── _overlay.scss │ │ ├── _p5-contrast-codemirror-theme.scss │ │ ├── _p5-dark-codemirror-theme.scss │ │ ├── _p5-light-codemirror-theme.scss │ │ ├── _preferences.scss │ │ ├── _preview-frame.scss │ │ ├── _preview-nav.scss │ │ ├── _quick-add.scss │ │ ├── _reset-password.scss │ │ ├── _resizer.scss │ │ ├── _searchbar.scss │ │ ├── _share.scss │ │ ├── _sidebar.scss │ │ ├── _sketch-list.scss │ │ ├── _tabs.scss │ │ ├── _timer.scss │ │ ├── _toast.scss │ │ ├── _toolbar.scss │ │ └── _uploader.scss │ ├── layout │ │ ├── _dashboard.scss │ │ └── _ide.scss │ └── main.scss ├── test-utils.js ├── testData │ ├── testReduxStore.js │ └── testServerResponses.js ├── theme.js └── utils │ ├── __mocks__ │ └── generateRandomName.js │ ├── apiClient.js │ ├── auth.js │ ├── codemirror-search.js │ ├── consoleUtils.js │ ├── device.js │ ├── dispatcher.js │ ├── evaluateExpression.js │ ├── formatDate.js │ ├── generateRandomName.js │ ├── getConfig.js │ ├── getConfig.test.js │ ├── htmlmixed.js │ ├── isSecurePage.js │ ├── metaKey.js │ ├── p5-hinter.js │ ├── p5-javascript.js │ ├── p5-keywords.js │ ├── previewEntry.js │ ├── reduxFormUtils.js │ └── responsive.jsx ├── contributor_docs ├── README.md ├── accessibility.md ├── deployment.md ├── development.md ├── installation.md ├── preparing_a_pull_request.md ├── public_api.md ├── public_api_proposed.md ├── release.md ├── testing.md └── translations.md ├── deploy.sh ├── deploy_staging.sh ├── docker-compose-development.yml ├── docker-compose.yml ├── index.js ├── infrastructure ├── README.md ├── images │ ├── control_plane_upgrade_1.png │ ├── control_plane_upgrade_2.png │ ├── node_pool_upgrade_1.png │ └── node_pool_upgrade_2.png └── terraform │ ├── .terraform.lock.hcl │ ├── README.md │ ├── backend.tf │ ├── main.tf │ ├── variables.tf │ └── vpc.tf ├── kubernetes_app.yml ├── nodemon.json ├── package-lock.json ├── package.json ├── public ├── code-of-conduct.md ├── favicon.ico ├── privacy-policy.md └── terms-of-use.md ├── server ├── config │ └── passport.js ├── controllers │ ├── __mocks__ │ │ └── aws.controller.js │ ├── aws.controller.js │ ├── collection.controller │ │ ├── addProjectToCollection.js │ │ ├── collectionForUserExists.js │ │ ├── createCollection.js │ │ ├── index.js │ │ ├── listCollections.js │ │ ├── removeCollection.js │ │ ├── removeProjectFromCollection.js │ │ └── updateCollection.js │ ├── embed.controller.js │ ├── file.controller.js │ ├── project.controller.js │ ├── project.controller │ │ ├── __test__ │ │ │ ├── createProject.test.js │ │ │ ├── deleteProject.test.js │ │ │ └── getProjectsForUser.test.js │ │ ├── createProject.js │ │ ├── deleteProject.js │ │ └── getProjectsForUser.js │ ├── session.controller.js │ ├── user.controller.js │ └── user.controller │ │ ├── __tests__ │ │ └── apiKey.test.js │ │ └── apiKey.js ├── domain-objects │ ├── Project.js │ ├── __test__ │ │ └── Project.test.js │ └── createDefaultFiles.js ├── jest.setup.js ├── migrations │ ├── db_reformat.js │ ├── db_reformat_start.js │ ├── emailConsolidation.js │ ├── moveBucket.js │ ├── populateTotalSize.js │ ├── s3UnderUser.js │ ├── start.js │ └── truncate.js ├── models │ ├── __mocks__ │ │ ├── project.js │ │ └── user.js │ ├── __test__ │ │ └── project.test.js │ ├── collection.js │ ├── project.js │ └── user.js ├── previewServer.js ├── routes │ ├── api.routes.js │ ├── asset.routes.js │ ├── aws.routes.js │ ├── collection.routes.js │ ├── embed.routes.js │ ├── file.routes.js │ ├── passport.routes.js │ ├── project.routes.js │ ├── redirectEmbed.routes.js │ ├── server.routes.js │ ├── session.routes.js │ └── user.routes.js ├── scripts │ ├── examples-gg-latest.js │ ├── examples-ml5.js │ ├── examples.js │ ├── fetch-examples-gg.js │ ├── fetch-examples-ml5.js │ ├── fetch-examples.js │ ├── update-p5-hinter.js │ └── update-syntax-highlighting.js ├── server.js ├── utils │ ├── __mocks__ │ │ └── createId.js │ ├── createApplicationErrorClass.js │ ├── createId.js │ ├── filePath.js │ ├── fileUtils.js │ ├── generateFileSystemSafeName.js │ ├── isAuthenticated.js │ ├── mail.js │ ├── previewGeneration.js │ ├── renderMjml.js │ └── requestsOfType.js └── views │ ├── 404Page.js │ ├── consolidationMailLayout.js │ ├── index.js │ ├── mail.js │ ├── mailLayout.js │ └── previewIndex.js ├── translations ├── contributor_docs │ ├── ko │ │ ├── CONTRIBUTING.md │ │ ├── README.md │ │ └── installation.md │ └── pt-br │ │ ├── README.md │ │ ├── accessibility.md │ │ ├── deployment.md │ │ ├── development.md │ │ ├── installation.md │ │ ├── preparing_a_pull_request.md │ │ ├── public_api.md │ │ └── public_api_proposed.md └── locales │ ├── de │ └── translations.json │ ├── en-US │ └── translations.json │ ├── es-419 │ └── translations.json │ ├── fr-CA │ └── translations.json │ ├── hi │ └── translations.json │ ├── it │ └── translations.json │ ├── ja │ └── translations.json │ ├── ko │ └── translations.json │ ├── pt-BR │ └── translations.json │ ├── sv │ └── translations.json │ ├── tr │ └── translations.json │ ├── uk-UA │ └── translations.json │ ├── zh-CN │ └── translations.json │ └── zh-TW │ └── translations.json └── webpack ├── config.dev.js ├── config.examples.js ├── config.prod.js └── config.server.js /.env.example: -------------------------------------------------------------------------------- 1 | API_URL=/editor 2 | AWS_ACCESS_KEY= 3 | AWS_REGION= 4 | AWS_SECRET_KEY= 5 | CORS_ALLOW_LOCALHOST=true 6 | EMAIL_SENDER= 7 | EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production 8 | EXAMPLE_USER_EMAIL=examples@p5js.org 9 | EXAMPLE_USER_PASSWORD=hellop5js 10 | GG_EXAMPLES_USERNAME=generativedesign 11 | GG_EXAMPLES_EMAIL=benedikt.gross@generative-gestaltung.de 12 | GG_EXAMPLES_PASS=generativedesign 13 | GITHUB_ID= 14 | GITHUB_SECRET= 15 | GOOGLE_ID= (use google+ api) 16 | GOOGLE_SECRET= (use google+ api) 17 | MAILGUN_DOMAIN= 18 | MAILGUN_KEY= 19 | ML5_LIBRARY_USERNAME=ml5 20 | ML5_LIBRARY_EMAIL=examples@ml5js.org 21 | ML5_LIBRARY_PASS=helloml5 22 | MOBILE_ENABLED=true 23 | MONGO_URL=mongodb://localhost:27017/p5js-web-editor 24 | PORT=8000 25 | PREVIEW_PORT=8002 26 | EDITOR_URL=http://localhost:8000 27 | PREVIEW_URL=http://localhost:8002 28 | S3_BUCKET= 29 | S3_BUCKET_URL_BASE= 30 | SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production 31 | TRANSLATIONS_ENABLED=true 32 | UI_ACCESS_TOKEN_ENABLED=false 33 | UPLOAD_LIMIT=250000000 34 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | index.js -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: processing 2 | custom: https://processingfoundation.org/ 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: 🌸 p5.js Issues 5 | url: https://github.com/processing/p5.js/issues 6 | about: Report issues with the p5.js here. 7 | - name: 🌐 Website Issues 8 | url: https://github.com/processing/p5.js-website/issues 9 | about: Report issues with the p5.js website here. 10 | - name: 💬 Forum 11 | url: https://discourse.processing.org/c/p5js 12 | about: Have other questions about using p5.js? Ask them here! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Existing Feature Enhancement 2 | description: Suggest an improvement to an existing feature. 3 | labels: [ Enhancement ] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Increasing Access 8 | description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to the p5.js Web Editor? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) 9 | validations: 10 | required: true 11 | 12 | - type: textarea 13 | attributes: 14 | label: Feature enhancement details 15 | validations: 16 | required: true 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 🌱 New Feature Request 2 | description: Request a new feature be added. 3 | labels: [ Feature Request ] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Increasing Access 8 | description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to the p5.js Web Editor? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) 9 | validations: 10 | required: true 11 | 12 | - type: textarea 13 | attributes: 14 | label: Feature request details 15 | validations: 16 | required: true 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes #issue-number 2 | 3 | Changes: 4 | 5 | I have verified that this pull request: 6 | 7 | * [ ] has no linting errors (`npm run lint`) 8 | * [ ] has no test errors (`npm run test`) 9 | * [ ] is from a uniquely-named feature branch and is up to date with the `develop` branch. 10 | * [ ] is descriptively named and links to an issue number, i.e. `Fixes #123` 11 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for welcome - https://github.com/behaviorbot/welcome 2 | 3 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 4 | 5 | # Comment to be posted to on first time issues 6 | newIssueWelcomeComment: > 7 | Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, be sure to follow the issue template if you haven't already. 8 | 9 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 10 | 11 | # Comment to be posted to on PRs from first time contributors in your repository 12 | newPRWelcomeComment: > 13 | 🎉 Thanks for opening this pull request! Please check out our [contributing guidelines](https://github.com/processing/p5.js-web-editor/blob/develop/.github/CONTRIBUTING.md) if you haven't already. 14 | 15 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 16 | 17 | # Comment to be posted to on pull requests merged by a first time user 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test and lint code base 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: '16.14.x' 15 | - run: npm install 16 | - run: npm run test 17 | - run: npm run lint 18 | 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .env.production 4 | .env.staging 5 | .vscode/ 6 | node_modules/ 7 | npm-debug.log 8 | dump.rdb 9 | static/dist/ 10 | static/css/app.min.css 11 | dist/ 12 | alpha_editor_p5js_org.key 13 | alpha_editor_p5js_org.ca-bundle 14 | alpha_editor_p5js_org.crt 15 | cert_chain.crt 16 | localhost.crt 17 | localhost.key 18 | privkey.pem 19 | terraform/.terraform/ 20 | 21 | storybook-static 22 | duplicates.json 23 | 24 | coverage -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.14.2 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "parser": "babel", 9 | "printWidth": 80, 10 | "proseWrap": "never", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "none", 16 | "useTabs": false, 17 | "quoteProps": "as-needed", 18 | "endOfLine":"auto" 19 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react-webpack5').StorybookConfig } */ 2 | const config = { 3 | stories: ['../client/**/*.stories.(jsx|mdx)'], 4 | addons: [ 5 | '@storybook/addon-links', 6 | '@storybook/addon-essentials', 7 | '@storybook/addon-interactions' 8 | ], 9 | framework: { 10 | name: '@storybook/react-webpack5', 11 | options: {} 12 | }, 13 | docs: { 14 | autodocs: 'tag' 15 | }, 16 | async webpackFinal(config) { 17 | // https://storybook.js.org/docs/react/builders/webpack 18 | // this modifies the existing image rule to exclude .svg files 19 | // since we want to handle those files with @svgr/webpack 20 | const imageRule = config.module.rules.find(rule => rule.test.test('.svg')) 21 | imageRule.exclude = /\.svg$/ 22 | 23 | // configure .svg files to be loaded with @svgr/webpack 24 | config.module.rules.push({ 25 | test: /\.svg$/, 26 | use: ['@svgr/webpack'] 27 | }) 28 | 29 | return config 30 | }, 31 | }; 32 | 33 | export default config; 34 | 35 | 36 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { MemoryRouter } from 'react-router'; 4 | 5 | import ThemeProvider from '../client/modules/App/components/ThemeProvider'; 6 | import configureStore from '../client/store'; 7 | import '../client/i18n-test'; 8 | import '../client/styles/storybook.css' 9 | 10 | const initialState = window.__INITIAL_STATE__; 11 | 12 | const store = configureStore(initialState); 13 | 14 | export const decorators = [ 15 | (Story) => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ), 24 | ] 25 | 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - "16.14.2" 5 | 6 | cache: 7 | directories: 8 | - "$HOME/google-cloud-sdk/" 9 | 10 | services: 11 | - docker 12 | 13 | before_install: 14 | - docker-compose -f docker-compose-development.yml up -d 15 | - docker ps -a 16 | 17 | install: true 18 | 19 | jobs: 20 | include: 21 | - stage: test 22 | name: "Linting" 23 | script: docker-compose exec -T app npm run lint 24 | - # stage name not required, will continue to use `test` 25 | name: "Tests" 26 | script: docker-compose exec -T app npm run test 27 | 28 | before_deploy: 29 | - docker-compose stop 30 | - if [ ! -d "$HOME/google-cloud-sdk/bin" ]; then rm -rf $HOME/google-cloud-sdk; export CLOUDSDK_CORE_DISABLE_PROMPTS=1; curl https://sdk.cloud.google.com | bash >/dev/null; fi 31 | - source /home/travis/google-cloud-sdk/path.bash.inc 32 | - gcloud --quiet version 33 | - gcloud --quiet components update 34 | - gcloud --quiet components update kubectl 35 | deploy: 36 | - provider: script 37 | script: ./deploy.sh 38 | skip_cleanup: true 39 | on: 40 | branch: release 41 | tags: true 42 | - provider: script 43 | script: ./deploy_staging.sh 44 | skip_cleanup: true 45 | on: 46 | branch: develop 47 | 48 | env: 49 | global: 50 | - APP_IMAGE_NAME=p5js-web-editor_app 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.14.2 as base 2 | ENV APP_HOME=/usr/src/app \ 3 | TERM=xterm 4 | RUN mkdir -p $APP_HOME 5 | WORKDIR $APP_HOME 6 | EXPOSE 8000 7 | EXPOSE 8002 8 | 9 | FROM base as development 10 | ENV NODE_ENV development 11 | COPY package.json package-lock.json ./ 12 | RUN npm install 13 | RUN npm rebuild node-sass 14 | COPY .babelrc index.js nodemon.json ./ 15 | COPY ./webpack ./webpack 16 | COPY client ./client 17 | COPY server ./server 18 | COPY translations/locales ./translations/locales 19 | COPY public ./public 20 | CMD ["npm", "start"] 21 | 22 | FROM development as build 23 | ENV NODE_ENV production 24 | RUN npm run build 25 | 26 | FROM base as production 27 | ENV NODE_ENV=production 28 | COPY package.json package-lock.json index.js ./ 29 | RUN npm install --production 30 | RUN npm rebuild node-sass 31 | COPY --from=build $APP_HOME/dist ./dist 32 | CMD ["npm", "run", "start:prod"] 33 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: MONGO_URL=$MONGO_URI MAILGUN_KEY=$MAILGUN_API_KEY npm run start:prod 2 | -------------------------------------------------------------------------------- /client/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /client/__mocks__/i18n.js: -------------------------------------------------------------------------------- 1 | import { enUS, es, ja, hi } from 'date-fns/locale'; 2 | import i18n from '../i18n-test'; 3 | 4 | export function languageKeyToLabel(lang) { 5 | const languageMap = { 6 | 'en-US': 'English', 7 | 'es-419': 'Español', 8 | ja: '日本語', 9 | hi: 'हिन्दी' 10 | }; 11 | return languageMap[lang]; 12 | } 13 | 14 | export function languageKeyToDateLocale(lang) { 15 | const languageMap = { 16 | 'en-US': enUS, 17 | 'es-419': es, 18 | ja, 19 | hi 20 | }; 21 | return languageMap[lang]; 22 | } 23 | 24 | export function currentDateLocale() { 25 | return languageKeyToDateLocale(i18n.language); 26 | } 27 | 28 | export default i18n; 29 | -------------------------------------------------------------------------------- /client/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /client/browserHistory.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | const browserHistory = createBrowserHistory(); 4 | 5 | export default browserHistory; 6 | -------------------------------------------------------------------------------- /client/common/ButtonOrLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | 5 | /** 6 | * Helper for switching between ; 24 | }; 25 | 26 | /** 27 | * Accepts all the props of an HTML or '); 17 | fireEvent.click(button); 18 | expect(clickHandler).toHaveBeenCalled(); 19 | }); 20 | 21 | it('can render an external link', () => { 22 | render(p5); 23 | const link = screen.getByRole('link'); 24 | expect(link).toBeInstanceOf(HTMLAnchorElement); 25 | expect(link).toHaveAttribute('href', 'https://p5js.org'); 26 | }); 27 | 28 | it('can render an internal link with react-router', async () => { 29 | render(About); 30 | 31 | const link = screen.getByText('About'); 32 | fireEvent.click(link); 33 | 34 | await waitFor(() => expect(history.location.pathname).toEqual('/about')); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /client/common/icons.stories.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as icons from './icons'; 4 | 5 | export default { 6 | title: 'Common/Icons', 7 | component: icons, 8 | argTypes: { 9 | variant: { 10 | options: Object.keys(icons), 11 | control: { type: 'select' }, 12 | default: icons.CircleFolderIcon 13 | } 14 | } 15 | }; 16 | 17 | export const Icons = (args) => { 18 | const SelectedIcon = icons[args.variant || 'CircleInfoIcon']; 19 | return ; 20 | }; 21 | -------------------------------------------------------------------------------- /client/components/AddRemoveButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import AddIcon from '../images/plus.svg'; 6 | import RemoveIcon from '../images/minus.svg'; 7 | 8 | const AddRemoveButton = ({ type, onClick }) => { 9 | const { t } = useTranslation(); 10 | const alt = 11 | type === 'add' 12 | ? t('AddRemoveButton.AltAddARIA') 13 | : t('AddRemoveButton.AltRemoveARIA'); 14 | const Icon = type === 'add' ? AddIcon : RemoveIcon; 15 | 16 | return ( 17 | 24 | ); 25 | }; 26 | 27 | AddRemoveButton.propTypes = { 28 | type: PropTypes.oneOf(['add', 'remove']).isRequired, 29 | onClick: PropTypes.func.isRequired 30 | }; 31 | 32 | export default AddRemoveButton; 33 | -------------------------------------------------------------------------------- /client/components/Nav/NavMenuItem.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { useContext, useMemo } from 'react'; 3 | import ButtonOrLink from '../../common/ButtonOrLink'; 4 | import { NavBarContext, ParentMenuContext } from './contexts'; 5 | 6 | function NavMenuItem({ hideIf, className, ...rest }) { 7 | const parent = useContext(ParentMenuContext); 8 | 9 | const { createMenuItemHandlers } = useContext(NavBarContext); 10 | 11 | const handlers = useMemo(() => createMenuItemHandlers(parent), [ 12 | createMenuItemHandlers, 13 | parent 14 | ]); 15 | 16 | if (hideIf) { 17 | return null; 18 | } 19 | 20 | return ( 21 |
  • 22 | 23 |
  • 24 | ); 25 | } 26 | 27 | NavMenuItem.propTypes = { 28 | ...ButtonOrLink.propTypes, 29 | onClick: PropTypes.func, 30 | value: PropTypes.string, 31 | /** 32 | * Provides a way to deal with optional items. 33 | */ 34 | hideIf: PropTypes.bool, 35 | className: PropTypes.string 36 | }; 37 | 38 | NavMenuItem.defaultProps = { 39 | onClick: null, 40 | value: null, 41 | hideIf: false, 42 | className: 'nav__dropdown-item' 43 | }; 44 | 45 | export default NavMenuItem; 46 | -------------------------------------------------------------------------------- /client/components/Nav/contexts.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const ParentMenuContext = createContext('none'); 4 | 5 | export const MenuOpenContext = createContext('none'); 6 | 7 | export const NavBarContext = createContext({ 8 | createDropdownHandlers: () => ({}), 9 | createMenuItemHandlers: () => ({}), 10 | toggleDropdownOpen: () => {} 11 | }); 12 | -------------------------------------------------------------------------------- /client/components/NavBasic.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { withTranslation } from 'react-i18next'; 4 | 5 | import LogoIcon from '../images/p5js-logo-small.svg'; 6 | import ArrowIcon from '../images/triangle-arrow-left.svg'; 7 | 8 | class NavBasic extends React.PureComponent { 9 | static defaultProps = { 10 | onBack: null 11 | }; 12 | 13 | render() { 14 | return ( 15 | 43 | ); 44 | } 45 | } 46 | 47 | NavBasic.propTypes = { 48 | onBack: PropTypes.func, 49 | t: PropTypes.func.isRequired 50 | }; 51 | 52 | export default withTranslation()(NavBasic); 53 | -------------------------------------------------------------------------------- /client/components/OverlayManager.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { createPortal } from 'react-dom'; 4 | 5 | const OverlayManager = ({ overlay, hideOverlay }) => { 6 | // const [visible, trigger, setRef] = useModalBehavior(); 7 | 8 | const jsx = ( 9 | 10 | {/*
    11 | {visible && } 12 |
    */} 13 |
    14 | ); 15 | 16 | return jsx && createPortal(jsx, document.body); 17 | }; 18 | 19 | OverlayManager.propTypes = { 20 | overlay: PropTypes.string, 21 | hideOverlay: PropTypes.func.isRequired 22 | }; 23 | 24 | OverlayManager.defaultProps = { overlay: null }; 25 | 26 | export default OverlayManager; 27 | -------------------------------------------------------------------------------- /client/components/RootPage.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { prop } from '../theme'; 3 | 4 | const RootPage = styled.div` 5 | min-height: 100%; 6 | display: flex; 7 | justify-content: start; 8 | flex-direction: column; 9 | color: ${prop('primaryTextColor')}; 10 | background-color: ${prop('backgroundColor')}; 11 | height: ${({ fixedHeight }) => fixedHeight || 'initial'}; 12 | 13 | @media (max-width: 770px) { 14 | height: 100%; 15 | overflow: hidden; 16 | } 17 | `; 18 | 19 | export default RootPage; 20 | -------------------------------------------------------------------------------- /client/components/createRedirectWithUsername.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import browserHistory from '../browserHistory'; 4 | 5 | const RedirectToUser = ({ username, url = '/:username/sketches' }) => { 6 | React.useEffect(() => { 7 | if (username == null) { 8 | return; 9 | } 10 | 11 | browserHistory.replace(url.replace(':username', username)); 12 | }, [username]); 13 | 14 | return null; 15 | }; 16 | 17 | function mapStateToProps(state) { 18 | return { 19 | username: state.user ? state.user.username : null 20 | }; 21 | } 22 | 23 | const ConnectedRedirectToUser = connect(mapStateToProps)(RedirectToUser); 24 | 25 | const createRedirectWithUsername = (url) => (props) => ( 26 | 27 | ); 28 | 29 | export default createRedirectWithUsername; 30 | -------------------------------------------------------------------------------- /client/components/mobile/ActionStrip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { remSize, prop } from '../../theme'; 5 | import IconButton from './IconButton'; 6 | 7 | const BottomBarContent = styled.div` 8 | padding: ${remSize(8)}; 9 | display: grid; 10 | grid-template-columns: repeat(8, 1fr); 11 | 12 | svg { 13 | max-height: ${remSize(32)}; 14 | } 15 | 16 | path { 17 | fill: ${prop('primaryTextColor')} !important; 18 | } 19 | 20 | .inverted { 21 | path { 22 | fill: ${prop('backgroundColor')} !important; 23 | } 24 | rect { 25 | fill: ${prop('primaryTextColor')} !important; 26 | } 27 | } 28 | `; 29 | 30 | const ActionStrip = ({ actions }) => ( 31 | 32 | {actions.map(({ icon, aria, action, inverted }) => ( 33 | 41 | ))} 42 | 43 | ); 44 | 45 | ActionStrip.propTypes = { 46 | actions: PropTypes.arrayOf( 47 | PropTypes.shape({ 48 | icon: PropTypes.component, 49 | aria: PropTypes.string.isRequired, 50 | action: PropTypes.func.isRequired, 51 | inverted: PropTypes.bool 52 | }) 53 | ).isRequired 54 | }; 55 | 56 | export default ActionStrip; 57 | -------------------------------------------------------------------------------- /client/components/mobile/Explorer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import PropTypes from 'prop-types'; 4 | import Sidebar from './Sidebar'; 5 | import ConnectedFileNode from '../../modules/IDE/components/FileNode'; 6 | 7 | const Explorer = ({ id, canEdit, onPressClose }) => { 8 | const { t } = useTranslation(); 9 | return ( 10 | 11 | onPressClose()} 15 | /> 16 | 17 | ); 18 | }; 19 | 20 | Explorer.propTypes = { 21 | id: PropTypes.number.isRequired, 22 | onPressClose: PropTypes.func, 23 | canEdit: PropTypes.bool 24 | }; 25 | Explorer.defaultProps = { 26 | canEdit: false, 27 | onPressClose: () => {} 28 | }; 29 | 30 | export default Explorer; 31 | -------------------------------------------------------------------------------- /client/components/mobile/FloatingNav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { remSize, prop } from '../../theme'; 5 | import IconButton from './IconButton'; 6 | 7 | const FloatingContainer = styled.div` 8 | position: fixed; 9 | right: ${remSize(16)}; 10 | top: ${remSize(80)}; 11 | 12 | text-align: right; 13 | z-index: 3; 14 | 15 | svg { 16 | width: ${remSize(32)}; 17 | } 18 | svg > path { 19 | fill: ${prop('Button.primary.default.background')} !important; 20 | } 21 | `; 22 | 23 | const FloatingNav = ({ items }) => ( 24 | 25 | {items.map(({ icon, onPress }) => ( 26 | 27 | ))} 28 | 29 | ); 30 | 31 | FloatingNav.propTypes = { 32 | items: PropTypes.arrayOf( 33 | PropTypes.shape({ 34 | icon: PropTypes.element, 35 | onPress: PropTypes.func 36 | }) 37 | ) 38 | }; 39 | 40 | FloatingNav.defaultProps = { 41 | items: [] 42 | }; 43 | 44 | export default FloatingNav; 45 | -------------------------------------------------------------------------------- /client/components/mobile/Footer.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { prop } from '../../theme'; 3 | 4 | const background = prop('MobilePanel.default.background'); 5 | const textColor = prop('primaryTextColor'); 6 | 7 | export default styled.div` 8 | position: fixed; 9 | width: 100%; 10 | bottom: 0; 11 | background: ${background}; 12 | color: ${textColor}; 13 | 14 | & > * + * { 15 | border-top: dashed 1px ${prop('Separator')}; 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /client/components/mobile/IDEWrapper.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { remSize } from '../../theme'; 3 | 4 | // Applies padding to top and bottom so editor content is always visible 5 | 6 | export default styled.div` 7 | z-index: 0; 8 | margin-top: ${remSize(16)}; 9 | .CodeMirror-sizer > * { 10 | padding-bottom: ${remSize(320)}; 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /client/components/mobile/IconButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import Button from '../../common/Button'; 5 | import { remSize } from '../../theme'; 6 | 7 | const ButtonWrapper = styled(Button)` 8 | width: ${remSize(48)}; 9 | > svg { 10 | width: 100%; 11 | height: 100%; 12 | } 13 | `; 14 | 15 | const IconButton = (props) => { 16 | const { icon, ...otherProps } = props; 17 | const Icon = icon; 18 | 19 | return ( 20 | } 22 | display={Button.displays.inline} 23 | focusable="false" 24 | {...otherProps} 25 | /> 26 | ); 27 | }; 28 | 29 | IconButton.propTypes = { 30 | icon: PropTypes.func 31 | }; 32 | 33 | IconButton.defaultProps = { 34 | icon: null 35 | }; 36 | 37 | export default IconButton; 38 | -------------------------------------------------------------------------------- /client/components/mobile/MobileScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { remSize, prop } from '../../theme'; 5 | 6 | const ScreenWrapper = styled.div` 7 | .toast { 8 | font-size: ${remSize(12)}; 9 | padding: ${remSize(8)}; 10 | 11 | border-radius: ${remSize(4)}; 12 | width: 92%; 13 | top: unset; 14 | min-width: unset; 15 | bottom: ${remSize(64)}; 16 | } 17 | ${({ fullscreen }) => 18 | fullscreen && 19 | ` 20 | display: flex; 21 | width: 100%; 22 | height: 100%; 23 | flex-flow: column; 24 | background-color: ${prop('backgroundColor')} 25 | `} 26 | `; 27 | 28 | const Screen = ({ children, fullscreen, slimheader }) => ( 29 | 30 | {children} 31 | 32 | ); 33 | 34 | Screen.defaultProps = { 35 | fullscreen: false, 36 | slimheader: false 37 | }; 38 | 39 | Screen.propTypes = { 40 | children: PropTypes.node.isRequired, 41 | fullscreen: PropTypes.bool, 42 | slimheader: PropTypes.bool 43 | }; 44 | 45 | export default Screen; 46 | -------------------------------------------------------------------------------- /client/components/mobile/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { remSize, prop } from '../../theme'; 5 | import Header from './Header'; 6 | import IconButton from './IconButton'; 7 | import { ExitIcon } from '../../common/icons'; 8 | 9 | const SidebarWrapper = styled.div` 10 | height: 100%; 11 | width: ${remSize(180)}; 12 | 13 | position: fixed; 14 | z-index: 2; 15 | left: 0; 16 | 17 | background: ${prop('backgroundColor')}; 18 | box-shadow: 0 6px 6px 0 rgba(0, 0, 0, 0.1); 19 | `; 20 | 21 | const Sidebar = ({ title, onPressClose, children }) => ( 22 | 23 | {title && ( 24 |
    25 | 30 |
    31 | )} 32 | {children} 33 |
    34 | ); 35 | 36 | Sidebar.propTypes = { 37 | title: PropTypes.string, 38 | onPressClose: PropTypes.func, 39 | children: PropTypes.oneOfType([ 40 | PropTypes.element, 41 | PropTypes.arrayOf(PropTypes.element) 42 | ]) 43 | }; 44 | 45 | Sidebar.defaultProps = { 46 | title: null, 47 | children: [], 48 | onPressClose: () => {} 49 | }; 50 | 51 | export default Sidebar; 52 | -------------------------------------------------------------------------------- /client/components/mobile/Tab.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Link } from 'react-router-dom'; 3 | import { prop, remSize } from '../../theme'; 4 | 5 | export default styled(Link)` 6 | box-sizing: border-box; 7 | 8 | background: transparent; 9 | /* border-top: ${remSize(4)} solid ${(props) => 10 | prop( 11 | props.selected ? 'colors.p5jsPink' : 'MobilePanel.default.background' 12 | )}; */ 13 | border-top: ${remSize(4)} solid 14 | ${(props) => (props.selected ? prop('TabHighlight') : 'transparent')}; 15 | 16 | color: ${prop('primaryTextColor')}; 17 | 18 | padding: ${remSize(8)} ${remSize(16)}; 19 | width: 30%; 20 | `; 21 | -------------------------------------------------------------------------------- /client/components/mobile/TabSwitcher.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { prop } from '../../theme'; 4 | 5 | export default styled.div` 6 | display: flex; 7 | justify-content: space-between; 8 | 9 | h3 { 10 | text-align: center; 11 | width: 100%; 12 | } 13 | border-top: 1px solid ${prop('Separator')}; 14 | 15 | background: ${(props) => prop('backgroundColor')}; 16 | `; 17 | -------------------------------------------------------------------------------- /client/components/useAsModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useModalBehavior } from '../modules/IDE/hooks/custom-hooks'; 4 | 5 | const BackgroundOverlay = styled.div` 6 | position: fixed; 7 | z-index: 2; 8 | width: 100% !important; 9 | height: 100% !important; 10 | 11 | background: black; 12 | opacity: 0.3; 13 | `; 14 | 15 | export default (Element, hasOverlay = false) => { 16 | const [visible, toggle, setRef] = useModalBehavior(); 17 | 18 | const wrapper = () => 19 | visible && ( 20 |
    21 | {hasOverlay && } 22 |
    23 | {typeof Element === 'function' ? Element(toggle) : Element} 24 |
    25 |
    26 | ); 27 | 28 | return [toggle, wrapper]; 29 | }; 30 | -------------------------------------------------------------------------------- /client/i18n-test.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | import translations from '../translations/locales/en-US/translations.json'; 5 | 6 | i18n.use(initReactI18next).init({ 7 | lng: 'en-US', 8 | fallbackLng: 'en-US', 9 | 10 | // have a common namespace used around the full app 11 | ns: ['translations'], 12 | defaultNS: 'translations', 13 | 14 | debug: false, 15 | 16 | interpolation: { 17 | escapeValue: false // not needed for react!! 18 | }, 19 | 20 | resources: { 'en-US': { translations } } 21 | }); 22 | 23 | export default i18n; 24 | -------------------------------------------------------------------------------- /client/images/account.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/images/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/check_encircled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/images/circle-folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/circle-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/images/circle-terminal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /client/images/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/images/console-command-contrast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/images/console-command-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/images/console-command-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/images/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/down-arrow-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/images/down-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/images/down-filled-triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/images/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/folder-padded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/images/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/images/information.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/images/left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/images/magnifyingglass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/images/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/p5-asterisk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/images/p5js-square-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hkirat/p5.js-web-editor/62df48791bfa139ec8e351f33e28e83c36de54ad/client/images/p5js-square-logo.png -------------------------------------------------------------------------------- /client/images/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/images/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/plus-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/right-arrow-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/images/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/images/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/images/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/images/sort-arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/images/sort-arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/images/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/terminal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/trash-can.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/images/triangle-arrow-down-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/triangle-arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/triangle-arrow-left.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/images/triangle-arrow-right-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/triangle-arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/unsaved-changes-dot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/images/up-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router } from 'react-router-dom'; 5 | 6 | import browserHistory from './browserHistory'; 7 | import configureStore from './store'; 8 | import Routing from './routes'; 9 | import ThemeProvider from './modules/App/components/ThemeProvider'; 10 | import Loader from './modules/App/components/loader'; 11 | import './i18n'; 12 | 13 | require('./styles/main.scss'); 14 | 15 | // Load the p5 png logo, so that webpack will use it 16 | require('./images/p5js-square-logo.png'); 17 | 18 | const initialState = window.__INITIAL_STATE__; 19 | 20 | const store = configureStore(initialState); 21 | 22 | const App = () => ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | render( 33 | }> 34 | 35 | , 36 | document.getElementById('root') 37 | ); 38 | -------------------------------------------------------------------------------- /client/index.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs/blocks'; 2 | 3 | 4 | 5 | # Welcome to the P5.js Web Editor Style Guide 6 | 7 | This guide will contain all the components in the project, with examples of how they can be reused. 8 | -------------------------------------------------------------------------------- /client/jest.setup.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import 'jest-styled-components'; 3 | import 'regenerator-runtime/runtime'; 4 | 5 | // See: https://github.com/testing-library/jest-dom 6 | // eslint-disable-next-line import/no-extraneous-dependencies 7 | import '@testing-library/jest-dom'; 8 | -------------------------------------------------------------------------------- /client/modules/App/components/DevTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from '@redux-devtools/core'; 3 | import { LogMonitor } from '@redux-devtools/log-monitor'; 4 | import { DockMonitor } from '@redux-devtools/dock-monitor'; 5 | 6 | const devTools = ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default createDevTools(devTools); 13 | -------------------------------------------------------------------------------- /client/modules/App/components/ThemeProvider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { ThemeProvider } from 'styled-components'; 5 | 6 | import theme, { Theme } from '../../../theme'; 7 | 8 | const Provider = ({ children, currentTheme }) => ( 9 | {children} 10 | ); 11 | 12 | Provider.propTypes = { 13 | children: PropTypes.node.isRequired, 14 | currentTheme: PropTypes.oneOf(Object.keys(Theme)).isRequired 15 | }; 16 | 17 | function mapStateToProps(state) { 18 | return { 19 | currentTheme: state.preferences.theme 20 | }; 21 | } 22 | 23 | export default connect(mapStateToProps)(Provider); 24 | -------------------------------------------------------------------------------- /client/modules/App/components/loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loader = () => ( 4 |
    5 |
    6 |
    7 |
    8 |
    9 |
    10 | ); 11 | export default Loader; 12 | -------------------------------------------------------------------------------- /client/modules/IDE/actions/assets.js: -------------------------------------------------------------------------------- 1 | import apiClient from '../../../utils/apiClient'; 2 | import * as ActionTypes from '../../../constants'; 3 | import { startLoader, stopLoader } from './loader'; 4 | 5 | function setAssets(assets, totalSize) { 6 | return { 7 | type: ActionTypes.SET_ASSETS, 8 | assets, 9 | totalSize 10 | }; 11 | } 12 | 13 | export function getAssets() { 14 | return (dispatch) => { 15 | dispatch(startLoader()); 16 | apiClient 17 | .get('/S3/objects') 18 | .then((response) => { 19 | dispatch(setAssets(response.data.assets, response.data.totalSize)); 20 | dispatch(stopLoader()); 21 | }) 22 | .catch(() => { 23 | dispatch({ 24 | type: ActionTypes.ERROR 25 | }); 26 | dispatch(stopLoader()); 27 | }); 28 | }; 29 | } 30 | 31 | export function deleteAsset(assetKey) { 32 | return { 33 | type: ActionTypes.DELETE_ASSET, 34 | key: assetKey 35 | }; 36 | } 37 | 38 | export function deleteAssetRequest(assetKey) { 39 | return (dispatch) => { 40 | apiClient 41 | .delete(`/S3/${assetKey}`) 42 | .then((response) => { 43 | dispatch(deleteAsset(assetKey)); 44 | }) 45 | .catch(() => { 46 | dispatch({ 47 | type: ActionTypes.ERROR 48 | }); 49 | }); 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /client/modules/IDE/actions/console.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../../../constants'; 2 | 3 | export function clearConsole() { 4 | return { 5 | type: ActionTypes.CLEAR_CONSOLE 6 | }; 7 | } 8 | 9 | export function dispatchConsoleEvent(messages) { 10 | return { 11 | type: ActionTypes.CONSOLE_EVENT, 12 | event: messages 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /client/modules/IDE/actions/editorAccessibility.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../../../constants'; 2 | 3 | export function updateLintMessage(severity, line, message) { 4 | return { 5 | type: ActionTypes.UPDATE_LINT_MESSAGE, 6 | severity, 7 | line, 8 | message 9 | }; 10 | } 11 | 12 | export function clearLintMessage() { 13 | return { 14 | type: ActionTypes.CLEAR_LINT_MESSAGE 15 | }; 16 | } 17 | 18 | export function toggleForceDesktop() { 19 | return { 20 | type: ActionTypes.TOGGLE_FORCE_DESKTOP 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /client/modules/IDE/actions/loader.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../../../constants'; 2 | 3 | export function startLoader() { 4 | return { type: ActionTypes.START_LOADING }; 5 | } 6 | 7 | export function stopLoader() { 8 | return { type: ActionTypes.STOP_LOADING }; 9 | } 10 | -------------------------------------------------------------------------------- /client/modules/IDE/actions/projects.js: -------------------------------------------------------------------------------- 1 | import apiClient from '../../../utils/apiClient'; 2 | import * as ActionTypes from '../../../constants'; 3 | import { startLoader, stopLoader } from './loader'; 4 | 5 | // eslint-disable-next-line 6 | export function getProjects(username) { 7 | return (dispatch) => { 8 | dispatch(startLoader()); 9 | let url; 10 | if (username) { 11 | url = `/${username}/projects`; 12 | } else { 13 | url = '/projects'; 14 | } 15 | return apiClient 16 | .get(url) 17 | .then((response) => { 18 | dispatch({ 19 | type: ActionTypes.SET_PROJECTS, 20 | projects: response.data 21 | }); 22 | dispatch(stopLoader()); 23 | }) 24 | .catch((error) => { 25 | const { response } = error; 26 | dispatch({ 27 | type: ActionTypes.ERROR, 28 | error: response.data 29 | }); 30 | dispatch(stopLoader()); 31 | }); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /client/modules/IDE/actions/projects.unit.test.js: -------------------------------------------------------------------------------- 1 | import configureStore from 'redux-mock-store'; 2 | import thunk from 'redux-thunk'; 3 | import { setupServer } from 'msw/node'; 4 | import { rest } from 'msw'; 5 | 6 | import * as ProjectActions from './projects'; 7 | import * as ActionTypes from '../../../constants'; 8 | import { 9 | initialTestState, 10 | mockProjects 11 | } from '../../../testData/testReduxStore'; 12 | 13 | const mockStore = configureStore([thunk]); 14 | 15 | const server = setupServer( 16 | rest.get(`/${initialTestState.user.username}/projects`, (req, res, ctx) => 17 | res(ctx.json(mockProjects)) 18 | ) 19 | ); 20 | 21 | beforeAll(() => server.listen()); 22 | afterEach(() => server.resetHandlers()); 23 | afterAll(() => server.close()); 24 | 25 | describe('projects action creator tests', () => { 26 | let store; 27 | 28 | afterEach(() => { 29 | store.clearActions(); 30 | }); 31 | 32 | it('creates GET_PROJECTS after successfuly fetching projects', () => { 33 | store = mockStore(initialTestState); 34 | 35 | const expectedActions = [ 36 | { type: ActionTypes.START_LOADING }, 37 | { type: ActionTypes.SET_PROJECTS, projects: mockProjects }, 38 | { type: ActionTypes.STOP_LOADING } 39 | ]; 40 | 41 | return store 42 | .dispatch(ProjectActions.getProjects('happydog')) 43 | .then(() => expect(store.getActions()).toEqual(expectedActions)); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /client/modules/IDE/actions/sorting.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../../../constants'; 2 | 3 | export const DIRECTION = { 4 | ASC: 'ASCENDING', 5 | DESC: 'DESCENDING' 6 | }; 7 | 8 | export function setSorting(field, direction) { 9 | return { 10 | type: ActionTypes.SET_SORTING, 11 | payload: { 12 | field, 13 | direction 14 | } 15 | }; 16 | } 17 | 18 | export function resetSorting() { 19 | return setSorting('createdAt', DIRECTION.DESC); 20 | } 21 | 22 | export function toggleDirectionForField(field) { 23 | return { 24 | type: ActionTypes.TOGGLE_DIRECTION, 25 | field 26 | }; 27 | } 28 | 29 | export function setSearchTerm(scope, searchTerm) { 30 | return { 31 | type: ActionTypes.SET_SEARCH_TERM, 32 | query: searchTerm, 33 | scope 34 | }; 35 | } 36 | 37 | export function resetSearchTerm(scope) { 38 | return setSearchTerm(scope, ''); 39 | } 40 | -------------------------------------------------------------------------------- /client/modules/IDE/actions/toast.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../../../constants'; 2 | 3 | export function hideToast() { 4 | return { 5 | type: ActionTypes.HIDE_TOAST 6 | }; 7 | } 8 | 9 | /** 10 | * Temporary fix until #2206 is merged. 11 | * Supports legacy two-action syntax: 12 | * dispatch(setToastText('Toast.SketchFailedSave')); 13 | * dispatch(showToast(1500)); 14 | * And also supports proposed single-action syntax with message and optional timeout. 15 | * dispatch(showToast('Toast.SketchFailedSave')); 16 | * dispatch(showToast('Toast.SketchSaved', 5500)); 17 | */ 18 | export function showToast(textOrTime, timeout = 1500) { 19 | return (dispatch) => { 20 | let time = timeout; 21 | if (typeof textOrTime === 'string') { 22 | // eslint-disable-next-line no-use-before-define 23 | dispatch(setToastText(textOrTime)); 24 | } else { 25 | time = textOrTime; 26 | } 27 | dispatch({ 28 | type: ActionTypes.SHOW_TOAST 29 | }); 30 | setTimeout(() => dispatch(hideToast()), time); 31 | }; 32 | } 33 | 34 | export function setToastText(text) { 35 | return { 36 | type: ActionTypes.SET_TOAST_TEXT, 37 | text 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /client/modules/IDE/components/AssetPreview.jsx: -------------------------------------------------------------------------------- 1 | import mime from 'mime'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | const Audio = styled.audio` 7 | width: 90%; 8 | margin: 30px 5%; 9 | `; 10 | 11 | const Image = styled.img` 12 | max-width: 100%; 13 | height: auto; 14 | `; 15 | 16 | const Video = styled.video` 17 | max-width: 100%; 18 | height: auto; 19 | `; 20 | 21 | function AssetPreview({ url, name }) { 22 | const contentType = mime.getType(url); 23 | const type = contentType?.split('/')[0]; 24 | 25 | switch (type) { 26 | case 'image': 27 | return {`Preview; 28 | case 'audio': 29 | // eslint-disable-next-line jsx-a11y/media-has-caption 30 | return