├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .storybook ├── main.js └── preview.js ├── .stylelintrc ├── LICENSE ├── README.md ├── assets ├── 3DStreet-Viewer-Start-Editor.svg ├── EditorLogo.svg ├── ScenePlaceholder.svg ├── ViewerLogo.svg ├── card-placeholder.svg ├── favicon.ico ├── gltf.svg ├── layers.svg └── logo-viewer.svg ├── config ├── .env.template ├── .gitignore └── README.md ├── dist ├── 3dstreet-editor.js ├── 3dstreet-editor.js.LICENSE.txt ├── 3dstreet-editor.js.map └── index.html ├── examples ├── colors.html ├── controllers.html ├── embedded-zoom.html ├── embedded.html ├── empty.html ├── index.html ├── js │ └── aframe.js └── supercraft.html ├── favicon.ico ├── index-3dtiles-cesium.html ├── index-intersection.html ├── index-mapbox-intersection.html ├── index-mapbox.html ├── index.html ├── package-lock.json ├── package.json ├── public ├── .firebaserc ├── .gitignore ├── apple-touch-icon.png ├── assets │ └── .gitignore ├── dist │ └── .gitignore ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions │ ├── .eslintrc.js │ ├── .gitignore │ ├── index.js │ ├── package-lock.json │ └── package.json └── index.html ├── src ├── api │ ├── auth.js │ ├── index.js │ ├── scene.js │ └── user.js ├── components │ ├── Collapsible.js │ ├── Main.js │ ├── MainWrapper.js │ ├── __tests__ │ │ └── Collapsible.test.js │ ├── components │ │ ├── AddComponent.js │ │ ├── AddLayerButton │ │ │ ├── AddLayerButton.component.jsx │ │ │ ├── AddLayerButton.module.scss │ │ │ └── index.js │ │ ├── AddLayerPanel │ │ │ ├── AddLayerPanel.component.jsx │ │ │ ├── AddLayerPanel.module.scss │ │ │ ├── cardsData.js │ │ │ ├── createLayerFunctions.js │ │ │ └── index.js │ │ ├── Button │ │ │ ├── Button.component.jsx │ │ │ ├── Button.module.scss │ │ │ └── index.js │ │ ├── Checkbox │ │ │ ├── Checkbox.component.jsx │ │ │ ├── Checkbox.module.scss │ │ │ └── index.js │ │ ├── CommonComponents.js │ │ ├── Component.js │ │ ├── ComponentsContainer.js │ │ ├── DefaultComponents.js │ │ ├── Dropdown │ │ │ ├── Dropdown.component.jsx │ │ │ ├── Dropdown.module.scss │ │ │ └── index.js │ │ ├── HelpButton │ │ │ ├── HelpButton.component.jsx │ │ │ ├── HelpButton.module.scss │ │ │ ├── icons.jsx │ │ │ └── index.js │ │ ├── Input │ │ │ ├── Input.component.jsx │ │ │ ├── Input.module.scss │ │ │ └── index.js │ │ ├── Logo │ │ │ ├── Logo.component.jsx │ │ │ ├── Logo.module.scss │ │ │ ├── Logo.stories.jsx │ │ │ ├── index.js │ │ │ └── logos.jsx │ │ ├── Mixins.js │ │ ├── ProfileButton │ │ │ ├── ProfileButton.component.jsx │ │ │ ├── ProfileButton.module.scss │ │ │ ├── icons.jsx │ │ │ └── index.js │ │ ├── PropertyRow.js │ │ ├── SceneCard │ │ │ ├── SceneCard.component.jsx │ │ │ ├── SceneCard.module.scss │ │ │ └── index.js │ │ ├── SceneEditTitle │ │ │ ├── SceneEditTitle.component.jsx │ │ │ ├── SceneEditTitle.module.scss │ │ │ └── index.js │ │ ├── ScreenshotButton │ │ │ ├── ScreenshotButton.component.jsx │ │ │ ├── ScreenshotButton.module.scss │ │ │ ├── icons.jsx │ │ │ └── index.js │ │ ├── Sidebar.js │ │ ├── Tabs │ │ │ ├── Tabs.component.jsx │ │ │ ├── Tabs.module.scss │ │ │ ├── components │ │ │ │ ├── Hint │ │ │ │ │ ├── Hint.component.jsx │ │ │ │ │ ├── Hint.scss │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── Toggle │ │ │ ├── Toggle.component.jsx │ │ │ ├── Toggle.module.scss │ │ │ └── index.js │ │ ├── ZoomButtons │ │ │ ├── ZoomButtons.component.jsx │ │ │ ├── ZoomButtons.module.scss │ │ │ └── index.js │ │ └── index.js │ ├── modals │ │ ├── Modal.jsx │ │ ├── Modal.module.scss │ │ ├── ModalHelp │ │ │ ├── ModalHelp.component.jsx │ │ │ ├── ModalHelp.module.scss │ │ │ ├── components │ │ │ │ ├── DocumentationButton │ │ │ │ │ ├── DocumentationButton.component.jsx │ │ │ │ │ ├── DocumentationButton.module.scss │ │ │ │ │ ├── icons.jsx │ │ │ │ │ └── index.js │ │ │ │ ├── EssentialActions │ │ │ │ │ ├── EssentialActions.component.jsx │ │ │ │ │ ├── EssentialActions.module.scss │ │ │ │ │ ├── icons.jsx │ │ │ │ │ └── index.js │ │ │ │ ├── Shortcuts │ │ │ │ │ ├── Shortcuts.component.jsx │ │ │ │ │ ├── Shortcuts.module.scss │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── ModalTextures.js │ │ ├── ProfileModal │ │ │ ├── ProfileModal.component.jsx │ │ │ ├── ProfileModal.module.scss │ │ │ ├── icons.jsx │ │ │ └── index.js │ │ ├── SavingModal │ │ │ ├── SavingModal.component.jsx │ │ │ ├── SavingModal.module.scss │ │ │ └── index.js │ │ ├── ScenesModal │ │ │ ├── ScenesModal.component.jsx │ │ │ ├── ScenesModal.module.scss │ │ │ └── index.js │ │ ├── ScreenshotModal │ │ │ ├── ScreenshotModal.component.jsx │ │ │ ├── ScreenshotModal.module.scss │ │ │ └── index.js │ │ └── SignInModal │ │ │ ├── SignInModal.component.jsx │ │ │ ├── SignInModal.module.scss │ │ │ └── index.js │ ├── scenegraph │ │ ├── Entity.js │ │ ├── SceneGraph.js │ │ ├── Toolbar.js │ │ └── ToolbarWrapper.js │ ├── viewport │ │ ├── CameraToolbar │ │ │ ├── CameraToolbar.component.js │ │ │ ├── CameraToolbar.scss │ │ │ └── index.js │ │ ├── TransformToolbar.js │ │ ├── ViewportHUD.js │ │ └── index.js │ └── widgets │ │ ├── BooleanWidget.js │ │ ├── ColorWidget.js │ │ ├── InputWidget.js │ │ ├── NumberWidget.js │ │ ├── SelectWidget.js │ │ ├── TextureWidget.js │ │ ├── Vec2Widget.js │ │ ├── Vec3Widget.js │ │ ├── Vec4Widget.js │ │ └── index.js ├── contexts │ ├── Auth.context.js │ └── index.js ├── hooks │ ├── index.js │ └── use-click-outside.js ├── icons │ ├── icons.jsx │ └── index.js ├── index.js ├── lib │ ├── EditorControls.js │ ├── Events.js │ ├── TransformControls.js │ ├── aframe-loader-3dtiles-component.min.js │ ├── assetsLoader.js │ ├── assetsUtils.js │ ├── cameras.js │ ├── entity.js │ ├── history.js │ ├── raycaster.js │ ├── shortcuts.js │ ├── toolbar.js │ ├── utils.js │ └── viewport.js ├── normalize.css ├── services │ ├── firebase.js │ └── ga.js ├── style │ ├── components.scss │ ├── entity.scss │ ├── index.scss │ ├── scenegraph.scss │ ├── select.scss │ ├── textureModal.scss │ ├── variables.scss │ ├── viewport.scss │ └── widgets.scss └── viewer-styles.css ├── webpack.config.js └── webpack.prod.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", ["@babel/preset-react", {"runtime": "automatic"}]] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib/vendor/ 2 | src/components/__tests__/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:react/jsx-runtime", "standard"], 3 | "rules": { 4 | "multiline-ternary": "off", 5 | "no-console": "off", 6 | "no-lone-blocks": "off", 7 | "no-var": "off", 8 | "object-shorthand": "off", 9 | "no-useless-return": "off", 10 | "prefer-const": "off", 11 | "react/jsx-indent-props": [2, 2], 12 | "semi": [2, "always"], 13 | "space-before-function-paren": "off" 14 | }, 15 | "env": { 16 | "browser": true, 17 | "es2021": true, 18 | "node": true 19 | }, 20 | "globals": { 21 | "AFRAME": true, 22 | "ga": true, 23 | "THREE": true 24 | }, 25 | "parser": "@babel/eslint-parser", 26 | "parserOptions": { 27 | "ecmaFeatures": { 28 | "jsx": true 29 | }, 30 | "ecmaVersion": "latest", 31 | "sourceType": "module" 32 | }, 33 | "plugins": [ 34 | "react" 35 | ], 36 | "settings": { 37 | "react": { 38 | "version": "detect" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test Cases 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | permissions: 10 | contents: read 11 | jobs: 12 | test: 13 | name: Test Cases 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: ['16.x'] 18 | steps: 19 | - name: Checkout Repo 20 | uses: actions/checkout@v3 21 | 22 | - name: Use Node.js ${{ matrix['node-version'] }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix['node-version'] }} 26 | cache: 'npm' 27 | cache-dependency-path: 'package-lock.json' 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Test Cases 33 | run: npm run test:ci 34 | 35 | - name: Check Lint 36 | run: npm run lint 37 | 38 | - name: Check Build 39 | run: npm run dist 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | gh-pages/ 4 | node_modules 5 | npm-debug.log* 6 | build/ 7 | *.sw[pomn] 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/lib/TransformControls.js 2 | dist -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react-webpack5').StorybookConfig } */ 2 | const config = { 3 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 4 | addons: [ 5 | '@storybook/addon-links', 6 | '@storybook/addon-essentials', 7 | '@storybook/addon-onboarding', 8 | '@storybook/addon-interactions', 9 | '@storybook/addon-styling-webpack', 10 | { 11 | name: '@storybook/addon-styling-webpack', 12 | options: { 13 | rules: [ 14 | { 15 | test: /\.css$/, 16 | sideEffects: true, 17 | use: [ 18 | require.resolve('style-loader'), 19 | { 20 | loader: require.resolve('css-loader'), 21 | options: { 22 | // Want to add more CSS Modules options? Read more here: https://github.com/webpack-contrib/css-loader#modules 23 | modules: { 24 | auto: true 25 | } 26 | } 27 | } 28 | ] 29 | }, 30 | { 31 | test: /\.s[ac]ss$/, 32 | sideEffects: true, 33 | use: [ 34 | require.resolve('style-loader'), 35 | { 36 | loader: require.resolve('css-loader'), 37 | options: { 38 | // Want to add more CSS Modules options? Read more here: https://github.com/webpack-contrib/css-loader#modules 39 | modules: { 40 | auto: true 41 | }, 42 | importLoaders: 2 43 | } 44 | }, 45 | require.resolve('resolve-url-loader'), 46 | { 47 | loader: require.resolve('sass-loader'), 48 | options: { 49 | // Want to add more Sass options? Read more here: https://webpack.js.org/loaders/sass-loader/#options 50 | implementation: require.resolve('sass'), 51 | sourceMap: true, 52 | sassOptions: {} 53 | } 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | } 60 | ], 61 | framework: { 62 | name: '@storybook/react-webpack5', 63 | options: { 64 | builder: { 65 | useSWC: true 66 | } 67 | } 68 | }, 69 | docs: { 70 | autodocs: 'tag' 71 | } 72 | }; 73 | 74 | export default config; 75 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../src/style/index.scss'; 2 | /** @type { import('@storybook/react').Preview } */ 3 | 4 | const preview = { 5 | parameters: { 6 | actions: { argTypesRegex: '^on[A-Z].*' }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/i 11 | } 12 | } 13 | } 14 | }; 15 | 16 | export default preview; 17 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "plugins": [ 4 | "stylelint-order" 5 | ], 6 | "rules": { 7 | "selector-type-no-unknown": [true, 8 | "ignoreTypes": ["a-scene", "a-entity"] 9 | ], 10 | "order/properties-alphabetical-order": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3DStreet Editor 2 | 3 | An editor tool for 3DStreet scenes. 4 | 5 | ## License and Source 6 | * This 3DStreet Editor repo is made available under the [AGPL 3.0 License](LICENSE). 7 | * The editor is a fork of the [A-Frame Inspector]() available under its own license terms. Subsequent changes to this editor repo are Copyright 2022 3DStreet LLC and made available for your use under the [AGPL 3.0 License](LICENSE). 8 | 9 | ## Local Development 10 | 11 | * First, clone the repo `git clone https://github.com/3DStreet/3dstreet-editor.git` 12 | * Then, ensure you have Firebase keys in .env.development in /config/ (see /config/README.md) 13 | * Then, run these commands from the `3dstreet-editor` repo root directory: 14 | 15 | ```bash 16 | npm install 17 | npm run start:dev 18 | ``` 19 | Then navigate to __[http://localhost:3333/](http://localhost:3333/)__ 20 | 21 | ### Testing production builds locally 22 | To test production builds locally, use the following steps from the `3dstreet-editor` repo root directory: 23 | 24 | ```bash 25 | npm run start:build 26 | npm run start:prod 27 | ``` 28 | 29 | ## Deployment instructions 30 | 31 | * Ensure you have .env.production in /config/ (see /config/README.md) 32 | * `npm run dist` 33 | * `npm run prefirebase` 34 | * `cd public` 35 | * `firebase use [PROJECT]` // ensure PROJECT matches target environment 36 | * ensure that in firebase.json the hosting SITE matches the target site for the project, such as "dev-3dstreet" or "app3dstreet" etc. -- [note this could be automated](https://stackoverflow.com/questions/61331567/firebase-cli-change-hosting-target) 37 | * `firebase deploy` 38 | 39 | Note: If you are deploying to a development server and want to use development (not production) firebase credentials, you'll need to copy your .env.development credentials to .env.production for the `npm run dist` step to ensure the output dist build uses the intended keys. (In other words, when running npm run dist it always uses firebase app credentials from .env.production, so be careful not to mistakingly deploy production firebase keys to a development server.) 40 | 41 | ## Release checklist (for Editor only) 42 | 43 | * after the above deployment instructions works on dev server (not deploy to production server yet) 44 | * bump the version on package.json & package-lock.json (for example from 0.4.1 to 0.4.2) 45 | * Use command line to create new tag for new version `git tag 0.4.2` and `git push --tags` 46 | * Make sure all of the above is committed and pushed to the repo 47 | * Then do production deployment following above instructions with .env.production environment 48 | * Create a new GitHub release here: https://github.com/3DStreet/3dstreet-editor/releases/new. Choose the tag you used above. (If needed for the title simply use the new version such as "1.1" or "1.1.0") 49 | * Click to automatically "generate release notes." Consider summarizing a few key changes to put at the top. 50 | * Update CHANGELOG.md with a quick summary of the auto generated release notes under the "Major improvement" heading. 51 | 52 | ## Editor Auth Notes 53 | 54 | * For testing we have a basic access control system using Firebase auth "claims." By default a user have no claims. They can be added in JSON format via direct database editing for a given user's claims field. Example claim for a user with beta access on a Pro plan: 55 | ``` 56 | { 57 | "plan": "PRO", 58 | "beta": true 59 | } 60 | ``` -------------------------------------------------------------------------------- /assets/EditorLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/ViewerLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/card-placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DStreet/3dstreet-editor/a28193d31377e70314c007a3249f24db89102a4d/assets/favicon.ico -------------------------------------------------------------------------------- /assets/layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /config/.env.template: -------------------------------------------------------------------------------- 1 | FIREBASE_API_KEY = 2 | FIREBASE_AUTH_DOMAIN = 3 | FIREBASE_PROJECT_ID = 4 | FIREBASE_STORAGE_BUCKET = 5 | FIREBASE_MESSAGING_SENDER_ID = 6 | FIREBASE_APP_ID = 7 | FIREBASE_MEASUREMENT_ID = 8 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | .env.development 2 | .env.production -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | For development you'll need a file `.env.development` in this directory. 2 | 3 | For production deployment you'll need a file `.env.production` in this directory. 4 | 5 | These files should NOT be committed to the repo and will be git ignored. 6 | 7 | You have 2 options to generate these files: 8 | * use .env.template to create your own .env.development and .env.production with GCP / Firebase keys generated on your own 9 | * or copy these .env files from GCP Secrets for the appropriate project / environment -------------------------------------------------------------------------------- /examples/colors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example Scene 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/controllers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Controllers 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /examples/embedded-zoom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inspector Test - Embedded (Bad) 6 | 7 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/embedded.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inspector Test - Embedded 6 | 7 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /examples/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example Scene 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example Scene 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /examples/supercraft.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DStreet/3dstreet-editor/a28193d31377e70314c007a3249f24db89102a4d/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3dstreet-editor", 3 | "version": "0.4.13", 4 | "description": "An open source street editor.", 5 | "main": "dist/3dstreet-editor.js", 6 | "scripts": { 7 | "start:dev": "webpack serve --config webpack.config.js --mode development --open --hot --progress", 8 | "start:prod": "serve dist -l 3333", 9 | "start:build": "webpack --config webpack.prod.config.js --mode production --progress", 10 | "dist": "npm run dist:max && npm run dist:min", 11 | "dist:max": "cross-env MINIFY=false npm run start:build", 12 | "dist:min": "cross-env MINIFY=true npm run start:build", 13 | "lint": "npm run lintfile 'src/**/*.js*'", 14 | "lint:css": "stylelint src/css/main.css", 15 | "lintfile": "eslint", 16 | "prefirebase": "cp -R assets public && cp {index.html,favicon.ico} public && cp -R dist public", 17 | "prepare": "husky install", 18 | "prepublish": "npm run dist", 19 | "prettier": "prettier --write 'src/**/*.js*'", 20 | "test": "jest --watch", 21 | "test:ci": "jest", 22 | "deploy": "npm run prefirebase && cd public && firebase deploy --only hosting:app3dstreet", 23 | "storybook": "storybook dev -p 6006", 24 | "build-storybook": "storybook build" 25 | }, 26 | "repository": "3dstreet/3dstreet-editor", 27 | "license": "AGPLv3", 28 | "dependencies": { 29 | "@types/uuid": "^9.0.0", 30 | "classnames": "^2.3.2", 31 | "clipboard": "^2.0.11", 32 | "date-fns": "^2.30.0", 33 | "dotenv-webpack": "^8.0.1", 34 | "file-loader": "^6.2.0", 35 | "firebase": "^9.23.0", 36 | "lodash-es": "^4.17.21", 37 | "prop-types": "^15.8.1", 38 | "react": "^18.2.0", 39 | "react-dom": "^18.2.0", 40 | "react-ga4": "^2.1.0", 41 | "react-select": "^5.4.0", 42 | "sass": "^1.69.5", 43 | "serve": "^14.2.1", 44 | "three": "0.145.0", 45 | "uuid": "^9.0.0" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.17.10", 49 | "@babel/eslint-parser": "7.19.1", 50 | "@babel/preset-env": "^7.17.10", 51 | "@babel/preset-react": "^7.17.12", 52 | "@storybook/addon-essentials": "^7.6.3", 53 | "@storybook/addon-interactions": "^7.6.3", 54 | "@storybook/addon-links": "^7.6.3", 55 | "@storybook/addon-onboarding": "^1.0.9", 56 | "@storybook/addon-styling-webpack": "^0.0.5", 57 | "@storybook/blocks": "^7.6.3", 58 | "@storybook/react": "^7.6.3", 59 | "@storybook/react-webpack5": "^7.6.3", 60 | "@storybook/test": "^7.6.3", 61 | "autoprefixer": "^10.4.12", 62 | "babel-jest": "^29.1.2", 63 | "babel-loader": "^8.2.5", 64 | "clean-webpack-plugin": "^4.0.0", 65 | "cross-env": "^7.0.3", 66 | "css-loader": "^6.8.1", 67 | "css-minimizer-webpack-plugin": "^5.0.1", 68 | "eslint": "^8.18.0", 69 | "eslint-config-standard": "^17.0.0", 70 | "eslint-plugin-react": "^7.30.1", 71 | "eslint-plugin-storybook": "^0.6.15", 72 | "html-webpack-plugin": "^5.5.3", 73 | "husky": "^8.0.1", 74 | "jest": "^29.1.2", 75 | "lint-staged": "^13.0.3", 76 | "mini-css-extract-plugin": "^2.7.2", 77 | "node-sass": "^8.0.0", 78 | "postcss-loader": "^7.0.0", 79 | "prettier": "^2.7.1", 80 | "react-test-renderer": "^18.2.0", 81 | "resolve-url-loader": "^5.0.0", 82 | "sass": "^1.69.5", 83 | "sass-loader": "^13.3.2", 84 | "storybook": "^7.6.3", 85 | "style-loader": "^3.3.3", 86 | "stylelint": "^14.13.0", 87 | "stylelint-config-standard": "^28.0.0", 88 | "stylelint-order": "^5.0.0", 89 | "stylus": "^0.59.0", 90 | "stylus-loader": "^7.1.0", 91 | "webpack": "^5.73.0", 92 | "webpack-cli": "^4.10.0", 93 | "webpack-dev-server": "^4.11.0" 94 | }, 95 | "keywords": [ 96 | "3d", 97 | "aframe", 98 | "editor", 99 | "inspector", 100 | "three.js", 101 | "tool", 102 | "unity", 103 | "vr", 104 | "virtualreality", 105 | "webvr", 106 | "wysiwyg" 107 | ], 108 | "lint-staged": { 109 | "*.js*": "prettier --write" 110 | }, 111 | "eslintConfig": { 112 | "extends": [ 113 | "plugin:storybook/recommended" 114 | ] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /public/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "dev-3dstreet" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DStreet/3dstreet-editor/a28193d31377e70314c007a3249f24db89102a4d/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore -------------------------------------------------------------------------------- /public/dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DStreet/3dstreet-editor/a28193d31377e70314c007a3249f24db89102a4d/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DStreet/3dstreet-editor/a28193d31377e70314c007a3249f24db89102a4d/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DStreet/3dstreet-editor/a28193d31377e70314c007a3249f24db89102a4d/public/favicon.ico -------------------------------------------------------------------------------- /public/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "site": "dev-3dstreet", 4 | "public": ".", 5 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 6 | "rewrites": [ 7 | { 8 | "source": "scenes/*.json", 9 | "function": "getScene" 10 | }, 11 | { 12 | "source": "**", 13 | "destination": "/index.html" 14 | } 15 | ] 16 | }, 17 | "functions": [ 18 | { 19 | "source": "functions", 20 | "codebase": "default", 21 | "ignore": [ 22 | "node_modules", 23 | ".git", 24 | "firebase-debug.log", 25 | "firebase-debug.*.log" 26 | ], 27 | "predeploy": ["npm --prefix \"$RESOURCE_DIR\" run lint"] 28 | } 29 | ], 30 | "emulators": { 31 | "functions": { 32 | "port": 5001 33 | }, 34 | "firestore": { 35 | "port": 8080 36 | }, 37 | "hosting": { 38 | "port": 5002 39 | }, 40 | "ui": { 41 | "enabled": true 42 | }, 43 | "singleProjectMode": true 44 | }, 45 | "firestore": { 46 | "rules": "firestore.rules", 47 | "indexes": "firestore.indexes.json" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /public/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | service cloud.firestore { 4 | match /databases/{database}/documents { 5 | match /scenes/{scene} { 6 | allow read; 7 | } 8 | 9 | match /scenes/{scene} { 10 | // Allow create new scene if user is authenticated 11 | allow create: if request.auth != null; 12 | 13 | // Allow update or delete scene if user is owner of document 14 | allow update, delete: if request.auth.uid == resource.data.author; 15 | } 16 | } 17 | } 18 | 19 | service firebase.storage { 20 | match /b/{bucket}/o { 21 | match /scenes/{scene_uuid}/files/{allPaths=**} { 22 | allow read: if isImageUnderSizeLimit() || request.auth != null; 23 | allow write: if request.auth != null && isAuthorOfScene(scene_uuid); 24 | } 25 | } 26 | } 27 | 28 | function isPublicAccess() { 29 | return request.auth == null; 30 | } 31 | 32 | function isImageUnderSizeLimit() { 33 | return resource.size < 100 * 1024 && resource.contentType.matches('image/.*'); 34 | } 35 | 36 | function isAuthorOfScene(scene_uuid) { 37 | return firestore.get(/databases/(default)/documents/scenes/$(scene_uuid)).data.author == request.auth.uid; 38 | } -------------------------------------------------------------------------------- /public/functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true 6 | }, 7 | extends: ['eslint:recommended', 'google'], 8 | rules: { 9 | quotes: ['error', 'double'] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /public/functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /public/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | admin.initializeApp(); 4 | 5 | exports.getScene = functions.https.onRequest(async (req, res) => { 6 | // Extract scene id from the path, remove the .json part 7 | res.set('Access-Control-Allow-Origin', '*'); 8 | const documentId = req.path 9 | .split('/') 10 | .filter(Boolean)[1] 11 | .replace('.json', ''); 12 | if (!documentId) { 13 | res.status(400).send({ error: 'Scene ID is required' }); 14 | return; 15 | } 16 | 17 | try { 18 | const doc = await admin 19 | .firestore() 20 | .collection('scenes') 21 | .doc(documentId) 22 | .get(); 23 | if (!doc.exists) { 24 | res 25 | .status(404) 26 | .send({ error: `Scene not found. DocumentID: ${documentId}` }); 27 | } else { 28 | res.send(doc.data()); 29 | } 30 | } catch (err) { 31 | res.status(500).send({ error: 'Error retrieving scene' }); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /public/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint", 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "16" 14 | }, 15 | "main": "index.js", 16 | "dependencies": { 17 | "firebase-admin": "^10.0.2", 18 | "firebase-functions": "^3.18.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^8.9.0", 22 | "eslint-config-google": "^0.14.0", 23 | "firebase-functions-test": "^0.2.0" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /src/api/auth.js: -------------------------------------------------------------------------------- 1 | import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; 2 | import { auth } from '../services/firebase'; 3 | import { sendMetric } from '../services/ga'; 4 | 5 | const signIn = async () => { 6 | try { 7 | const { 8 | user: { 9 | metadata: { creationTime, lastSignInTime } 10 | } 11 | } = await signInWithPopup(auth, new GoogleAuthProvider()); 12 | 13 | // first signIn to ga 14 | if (creationTime !== lastSignInTime) return; 15 | sendMetric('Auth', 'newAccountCreation'); 16 | } catch (error) { 17 | console.error(error); 18 | } 19 | }; 20 | 21 | export { signIn }; 22 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | getUserScenes, 3 | updateScene, 4 | isSceneAuthor, 5 | getCommunityScenes, 6 | checkIfImagePathIsEmpty 7 | } from './scene'; 8 | 9 | export { signIn } from './auth'; 10 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | const isUserPro = async (user) => { 2 | if (user) { 3 | try { 4 | await user.getIdToken(true); 5 | const idTokenResult = await user.getIdTokenResult(); 6 | if (idTokenResult.claims.plan === "PRO") { 7 | console.log("PRO PLAN USER"); 8 | return true; 9 | } else { 10 | console.log("FREE PLAN USER"); 11 | return false; 12 | } 13 | } catch (error) { 14 | console.error("Error checking PRO plan:", error); 15 | return false; 16 | } 17 | } else { 18 | console.log('refreshIdTokens: currentUser not set'); 19 | return false; 20 | } 21 | }; 22 | 23 | const isUserBeta = async (user) => { 24 | if (user) { 25 | try { 26 | await user.getIdToken(true); 27 | const idTokenResult = await user.getIdTokenResult(); 28 | if (idTokenResult.claims.beta) { 29 | console.log("BETA USER"); 30 | return true; 31 | } else { 32 | console.log("NOT BETA USER"); 33 | return false; 34 | } 35 | } catch (error) { 36 | console.error("Error checking BETA status:", error); 37 | return false; 38 | } 39 | } else { 40 | console.log('refreshIdTokens: currentUser not set'); 41 | return false; 42 | } 43 | }; 44 | 45 | export { 46 | isUserPro, 47 | isUserBeta 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/Collapsible.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import { sendMetric } from '../services/ga'; 5 | 6 | export default class Collapsible extends React.Component { 7 | static propTypes = { 8 | className: PropTypes.string, 9 | collapsed: PropTypes.bool, 10 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.element]) 11 | .isRequired, 12 | id: PropTypes.string 13 | }; 14 | 15 | static defaultProps = { 16 | collapsed: false 17 | }; 18 | 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | collapsed: this.props.collapsed 23 | }; 24 | } 25 | 26 | toggleVisibility = (event) => { 27 | // Don't collapse if we click on actions like clipboard 28 | if (event.target.nodeName === 'A') return; 29 | this.setState({ collapsed: !this.state.collapsed }); 30 | sendMetric('Components', 'collapse'); 31 | }; 32 | 33 | render() { 34 | const rootClassNames = { 35 | collapsible: true, 36 | component: true, 37 | collapsed: this.state.collapsed 38 | }; 39 | if (this.props.className) { 40 | rootClassNames[this.props.className] = true; 41 | } 42 | const rootClasses = classNames(rootClassNames); 43 | 44 | const contentClasses = classNames({ 45 | content: true, 46 | hide: this.state.collapsed 47 | }); 48 | 49 | return ( 50 |
51 |
52 |
53 | {this.props.children[0]} 54 |
55 |
{this.props.children[1]}
56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/MainWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuthContext } from '../contexts/index.js'; 3 | import Main from './Main'; 4 | 5 | const MainWrapper = (props) => { 6 | const { currentUser } = useAuthContext(); 7 | return
; 8 | }; 9 | 10 | export default MainWrapper; 11 | -------------------------------------------------------------------------------- /src/components/__tests__/Collapsible.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import Collapsible from '../Collapsible.js'; 5 | 6 | describe('Collapsible', () => { 7 | it('does not set class when not collapsed', () => { 8 | const tree = renderer 9 | .create( 10 | 11 |
12 |
13 | 14 | ) 15 | .toJSON(); 16 | expect(tree.children[1].props.className).not.toContain('hide'); 17 | }); 18 | 19 | it('sets class when collapsed', () => { 20 | const tree = renderer 21 | .create( 22 | 23 |
24 |
25 | 26 | ) 27 | .toJSON(); 28 | expect(tree.children[1].props.className).toContain('hide'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/components/AddComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Events from '../../lib/Events'; 4 | import Select from 'react-select'; 5 | import { sendMetric } from '../../services/ga'; 6 | 7 | export default class AddComponent extends React.Component { 8 | static propTypes = { 9 | entity: PropTypes.object 10 | }; 11 | 12 | /** 13 | * Add blank component. 14 | * If component is instanced, generate an ID. 15 | */ 16 | addComponent = (value) => { 17 | let componentName = value.value; 18 | 19 | var entity = this.props.entity; 20 | 21 | if (AFRAME.components[componentName].multiple) { 22 | const id = prompt( 23 | `Provide an ID for this component (e.g., 'foo' for ${componentName}__foo).` 24 | ); 25 | componentName = id ? `${componentName}__${id}` : componentName; 26 | } 27 | 28 | entity.setAttribute(componentName, ''); 29 | Events.emit('componentadd', { entity: entity, component: componentName }); 30 | sendMetric('Components', 'addComponent', componentName); 31 | }; 32 | 33 | /** 34 | * Component dropdown options. 35 | */ 36 | getComponentsOptions() { 37 | const usedComponents = Object.keys(this.props.entity.components); 38 | var commonOptions = Object.keys(AFRAME.components) 39 | .filter(function (componentName) { 40 | return ( 41 | AFRAME.components[componentName].multiple || 42 | usedComponents.indexOf(componentName) === -1 43 | ); 44 | }) 45 | .sort() 46 | .map(function (value) { 47 | return { value: value, label: value, origin: 'loaded' }; 48 | }); 49 | 50 | this.options = commonOptions; 51 | this.options = this.options.sort(function (a, b) { 52 | return a.label === b.label ? 0 : a.label < b.label ? -1 : 1; 53 | }); 54 | } 55 | 56 | renderOption(option) { 57 | var bullet = ( 58 | 59 | ); 60 | return ( 61 | 62 | {option.label} {option.origin === 'loaded' ? bullet : ''} 63 | 64 | ); 65 | } 66 | 67 | render() { 68 | const entity = this.props.entity; 69 | if (!entity) { 70 | return
; 71 | } 72 | 73 | this.getComponentsOptions(); 74 | 75 | return ( 76 |
77 |

COMPONENTS

78 | onChange(!isChecked)} 38 | checked={isChecked} 39 | type={'checkbox'} 40 | disabled={disabled} 41 | /> 42 | 68 |
69 | {label && ( 70 | 76 | )} 77 |
78 | ); 79 | }; 80 | 81 | Checkbox.propTypes = { 82 | isChecked: bool.isRequired, 83 | onChange: func.isRequired, 84 | label: string, 85 | className: string, 86 | id: string, 87 | disabled: bool 88 | }; 89 | 90 | export { Checkbox }; 91 | -------------------------------------------------------------------------------- /src/components/components/Checkbox/Checkbox.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .wrapper { 4 | display: flex; 5 | column-gap: 0.75rem; 6 | align-items: flex-start; 7 | width: fit-content; 8 | 9 | .checkboxContainer { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | width: 20px; 14 | min-width: 20px; 15 | height: 20px; 16 | min-height: 20px; 17 | 18 | .checkbox { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | width: 15px; 23 | height: 15px; 24 | border: 1.5px solid variables.$lightgray-400; 25 | border-radius: 3px; 26 | transition: all 0.2s; 27 | 28 | .checkedIcon { 29 | opacity: 1; 30 | visibility: visible; 31 | transition: all 0.2s; 32 | 33 | path { 34 | stroke: variables.$lightgray-400; 35 | transition: all 0.2s; 36 | } 37 | } 38 | 39 | .uncheckedIcon { 40 | opacity: 0; 41 | visibility: hidden; 42 | transition: all 0.2s; 43 | } 44 | 45 | &:hover { 46 | transition: all 0.2s; 47 | border-color: variables.$white; 48 | cursor: pointer; 49 | 50 | .uncheckedIcon { 51 | transition: all 0.2s; 52 | opacity: 0.2; 53 | color: variables.$black; 54 | visibility: visible; 55 | 56 | path { 57 | transition: all 0.2s; 58 | stroke: variables.$white; 59 | } 60 | } 61 | 62 | .checkedIcon path { 63 | transition: all 0.2s; 64 | stroke: variables.$white; 65 | } 66 | } 67 | 68 | &:active { 69 | transition: all 0.2s; 70 | border-color: #00000020; 71 | 72 | .checkedIcon, 73 | .uncheckedIcon { 74 | transition: all 0.2s; 75 | visibility: visible; 76 | opacity: 0.2; 77 | 78 | path { 79 | transition: all 0.2s; 80 | stroke: variables.$black; 81 | } 82 | } 83 | } 84 | } 85 | 86 | .disabledCheckbox, 87 | .disabledCheckbox:hover, 88 | .disabledCheckbox:active { 89 | border-color: variables.$gray-700; 90 | transition: all 0.2s; 91 | 92 | .checkedIcon { 93 | opacity: 1; 94 | 95 | path { 96 | transition: all 0.2s; 97 | stroke: variables.$gray-700; 98 | } 99 | } 100 | 101 | .uncheckedIcon { 102 | visibility: hidden; 103 | opacity: 0; 104 | } 105 | } 106 | } 107 | 108 | .label { 109 | display: flex; 110 | justify-content: flex-start; 111 | font-size: 1rem; 112 | line-height: 1.1875rem; 113 | font-weight: 400; 114 | color: variables.$white; 115 | transition: all 0.2s; 116 | 117 | &:hover { 118 | cursor: pointer; 119 | } 120 | } 121 | 122 | .disabledLabel { 123 | transition: all 0.3; 124 | color: variables.$gray-700; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/components/components/Checkbox/index.js: -------------------------------------------------------------------------------- 1 | export { Checkbox } from './Checkbox.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/ComponentsContainer.js: -------------------------------------------------------------------------------- 1 | import AddComponent from './AddComponent'; 2 | import CommonComponents from './CommonComponents'; 3 | import Component from './Component'; 4 | import DEFAULT_COMPONENTS from './DefaultComponents'; 5 | import PropTypes from 'prop-types'; 6 | import React from 'react'; 7 | export default class ComponentsContainer extends React.Component { 8 | static propTypes = { 9 | entity: PropTypes.object 10 | }; 11 | 12 | refresh = () => { 13 | this.forceUpdate(); 14 | }; 15 | 16 | render() { 17 | const entity = this.props.entity; 18 | const components = entity ? entity.components : {}; 19 | const definedComponents = Object.keys(components).filter((key) => { 20 | return DEFAULT_COMPONENTS.indexOf(key) === -1; 21 | }); 22 | const renderedComponents = definedComponents.sort().map((key, idx) => { 23 | return ( 24 |
25 | 2} 27 | component={components[key]} 28 | entity={entity} 29 | key={key} 30 | name={key} 31 | /> 32 |
33 | ); 34 | }); 35 | 36 | return ( 37 |
38 | 39 | 40 | {renderedComponents} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/components/DefaultComponents.js: -------------------------------------------------------------------------------- 1 | export default ['visible', 'position', 'scale', 'rotation']; 2 | -------------------------------------------------------------------------------- /src/components/components/Dropdown/Dropdown.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ArrowDown24Icon, ArrowUp24Icon } from '../../../icons'; 3 | import { arrayOf, bool, func, node, shape, string } from 'prop-types'; 4 | import { useRef, useState } from 'react'; 5 | 6 | import classNames from 'classnames'; 7 | import styles from './Dropdown.module.scss'; 8 | import { useClickOutside } from '../../../hooks'; 9 | 10 | /** 11 | * Dropdown component. 12 | * 13 | * @author Oleksii Medvediev 14 | * @category Components 15 | * @param {{ 16 | * options: Array<{value: string, label: string, disabled?: boolean}>; 17 | * selectedOptionValue?: string; 18 | * onSelect: (value: string) => void; 19 | * label?: string; 20 | * icon?: Element; 21 | * placeholder: string; 22 | * disabled?: boolean; 23 | * }} props 24 | */ 25 | const Dropdown = ({ 26 | options, 27 | selectedOptionValue, 28 | onSelect, 29 | label, 30 | icon, 31 | placeholder, 32 | disabled, 33 | smallDropdown, 34 | className 35 | }) => { 36 | const [isOptionsMenuOpen, setIsMenuOptionsOpen] = useState(false); 37 | const toggleOptionsMenu = () => 38 | setIsMenuOptionsOpen((prevState) => !prevState); 39 | 40 | const findSelectedOptionLabel = () => 41 | options.find(({ value }) => value === selectedOptionValue)?.label; 42 | 43 | const ref = useRef(null); 44 | 45 | useClickOutside(ref, () => setIsMenuOptionsOpen(false)); 46 | 47 | return ( 48 |
49 | {label && ( 50 | 53 | {label} 54 | 55 | )} 56 |
57 | 79 | {isOptionsMenuOpen && ( 80 |
81 | {!!options.length && 82 | options 83 | .sort((a, b) => { 84 | if ((a.disabled ?? false) > (b.disabled ?? false)) { 85 | return 1; 86 | } else if ((a.disabled ?? false) < (b.disabled ?? false)) { 87 | return -1; 88 | } else { 89 | return 0; 90 | } 91 | }) 92 | .map(({ value, label, disabled, onClick }, index) => ( 93 | 110 | ))} 111 |
112 | )} 113 |
114 |
115 | ); 116 | }; 117 | 118 | Dropdown.propTypes = { 119 | options: arrayOf( 120 | shape({ 121 | value: string.isRequired, 122 | label: string.isRequired, 123 | disabled: bool 124 | }) 125 | ).isRequired, 126 | selectedOptionValue: string, 127 | onSelect: func.isRequired, 128 | label: string, 129 | icon: node, 130 | placeholder: string.isRequired, 131 | disabled: bool 132 | }; 133 | 134 | export { Dropdown }; 135 | -------------------------------------------------------------------------------- /src/components/components/Dropdown/index.js: -------------------------------------------------------------------------------- 1 | export { Dropdown } from './Dropdown.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/HelpButton/HelpButton.component.jsx: -------------------------------------------------------------------------------- 1 | import styles from './HelpButton.module.scss'; 2 | 3 | import { Button } from '../Button'; 4 | import { Component } from 'react'; 5 | import Events from '../../../lib/Events.js'; 6 | import { QuestionMark } from './icons.jsx'; 7 | 8 | /** 9 | * HelpButton component. 10 | * 11 | * @author Anna Botsula, Oleksii Medvediev 12 | * @category Components. 13 | */ 14 | class HelpButton extends Component { 15 | render() { 16 | const onClick = () => Events.emit('openhelpmodal'); 17 | 18 | return ( 19 |
20 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | export { HelpButton }; 35 | -------------------------------------------------------------------------------- /src/components/components/HelpButton/HelpButton.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .wrapper { 4 | .helpButton { 5 | position: absolute; 6 | left: 32px; 7 | border: unset; 8 | bottom: 31px; 9 | border-radius: 50%; 10 | width: 43px; 11 | height: 43px; 12 | background-color: variables.$purple; 13 | padding: 0 !important; 14 | 15 | &:hover { 16 | background-color: variables.$purple-100; 17 | } 18 | 19 | &:active { 20 | background-color: variables.$purple-200; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/components/HelpButton/icons.jsx: -------------------------------------------------------------------------------- 1 | const QuestionMark = ( 2 | 9 | 13 | 14 | ); 15 | 16 | export { QuestionMark }; 17 | -------------------------------------------------------------------------------- /src/components/components/HelpButton/index.js: -------------------------------------------------------------------------------- 1 | export { HelpButton } from './HelpButton.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/Input/Input.component.jsx: -------------------------------------------------------------------------------- 1 | import { bool, func, node, string } from 'prop-types'; 2 | 3 | import classnames from 'classnames'; 4 | import styles from './Input.module.scss'; 5 | import { useRef } from 'react'; 6 | import { v4 } from 'uuid'; 7 | 8 | const id = v4(); 9 | 10 | /** 11 | * Input component. 12 | * 13 | * @author Oleksii Medvediev 14 | * @category Components 15 | * @param {{ 16 | * className?: string; 17 | * type?: string; 18 | * onChange: (value: string) => void; 19 | * leadingIcon?: Element; 20 | * leadingSubtext?: string; 21 | * tailingIcon?: Element; 22 | * tailingSubtext?: string; 23 | * label?: string; 24 | * inputId?: string; 25 | * placeholder?: string; 26 | * errorMessage?: string; 27 | * successMessage?: string; 28 | * disabled?: boolean; 29 | * readOnly?: boolean; 30 | * hideBorderAndBackground?: boolean; 31 | * }} props 32 | */ 33 | const Input = ({ 34 | className, 35 | type = 'text', 36 | onChange, 37 | leadingIcon, 38 | tailingIcon, 39 | leadingSubtext, 40 | tailingSubtext, 41 | label, 42 | inputId = id, 43 | placeholder, 44 | errorMessage, 45 | successMessage, 46 | defaultValue, 47 | disabled, 48 | readOnly, 49 | hideBorderAndBackground, 50 | value 51 | }) => { 52 | const inputElement = useRef(null); 53 | const hideBorderAndBackgroundStyles = { 54 | border: 'none', 55 | background: 'none', 56 | padding: '0px', 57 | width: 'calc(100% - 12px)' 58 | }; 59 | return ( 60 |
61 | {label && ( 62 | 68 | )} 69 |
{ 73 | inputElement.current.focus(); 74 | }} 75 | className={classnames( 76 | styles.inputElementContainer, 77 | disabled && styles.disabledInputContainer, 78 | errorMessage && 79 | !successMessage && 80 | !disabled && 81 | styles.erroredInputContainer, 82 | successMessage && 83 | !errorMessage && 84 | !disabled && 85 | styles.successInputContainer 86 | )} 87 | style={hideBorderAndBackground && hideBorderAndBackgroundStyles} 88 | > 89 | {leadingIcon && ( 90 |
{leadingIcon}
91 | )} 92 | {leadingSubtext && ( 93 | {leadingSubtext} 94 | )} 95 | onChange(event.currentTarget.value)} 99 | id={inputId} 100 | placeholder={placeholder} 101 | className={styles.inputElement} 102 | disabled={disabled} 103 | defaultValue={defaultValue} 104 | value={value} 105 | readOnly={readOnly} 106 | /> 107 | {tailingSubtext && ( 108 | {tailingSubtext} 109 | )} 110 | {tailingIcon && ( 111 |
{tailingIcon}
112 | )} 113 |
114 | {errorMessage && !successMessage && !disabled && ( 115 | {errorMessage} 116 | )} 117 | {successMessage && !errorMessage && !disabled && ( 118 | {successMessage} 119 | )} 120 |
121 | ); 122 | }; 123 | 124 | Input.propTypes = { 125 | className: string, 126 | type: string, 127 | onChange: func, 128 | leadingIcon: node, 129 | tailingIcon: node, 130 | leadingSubtext: string, 131 | tailingSubtext: string, 132 | label: string, 133 | inputId: string, 134 | placeholder: string, 135 | errorMessage: string, 136 | successMessage: string, 137 | disabled: bool, 138 | readOnly: bool, 139 | hideBorderAndBackground: bool 140 | }; 141 | 142 | export { Input }; 143 | -------------------------------------------------------------------------------- /src/components/components/Input/index.js: -------------------------------------------------------------------------------- 1 | export { Input } from './Input.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/Logo/Logo.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EditorLogo, ViewerLogo } from './logos.jsx'; 3 | 4 | import { Button } from '../Button'; 5 | import PropTypes from 'prop-types'; 6 | import styles from './Logo.module.scss'; 7 | 8 | /** 9 | * Logo component. 10 | * 11 | * @author Oleksii Medvediev 12 | * @category Components 13 | */ 14 | const Logo = ({ onToggleEdit, isEditor }) => ( 15 |
16 |
17 | {isEditor ? : } 18 |
19 | 22 |
23 | ); 24 | 25 | Logo.propTypes = { 26 | onToggleEdit: PropTypes.func, 27 | isEditor: PropTypes.bool 28 | }; 29 | 30 | export { Logo }; 31 | -------------------------------------------------------------------------------- /src/components/components/Logo/Logo.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .wrapper { 4 | position: absolute; 5 | display: flex; 6 | top: 36px; 7 | left: 40px; 8 | z-index: 10; 9 | display: flex; 10 | width: fit-content; 11 | align-items: center; 12 | column-gap: 1.5rem; 13 | } 14 | 15 | @media screen and (max-width: 1268px) { 16 | .wrapper { 17 | column-gap: 0rem; 18 | flex-direction: column; 19 | row-gap: 12px; 20 | left: 20px; 21 | top: 24px; 22 | .btn { 23 | width: 100%; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/components/Logo/Logo.stories.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Logo } from './Logo.component'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | export default { 6 | component: Logo, 7 | title: 'UI-KIT/Logo' 8 | }; 9 | 10 | const Default = { 11 | args: { 12 | Logo: { 13 | onToggleEdit: action('on toggle'), 14 | isEditor: false 15 | } 16 | } 17 | }; 18 | 19 | export { Default }; 20 | -------------------------------------------------------------------------------- /src/components/components/Logo/index.js: -------------------------------------------------------------------------------- 1 | export { Logo } from './Logo.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/ProfileButton/ProfileButton.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './ProfileButton.module.scss'; 3 | 4 | import { Button } from '../Button'; 5 | import Events from '../../../lib/Events.js'; 6 | import { Profile32Icon } from './icons.jsx'; 7 | import { useAuthContext } from '../../../contexts'; 8 | 9 | /** 10 | * ProfileButton component. 11 | * 12 | * @author Rostyslav Nahornyi 13 | * @category Components. 14 | */ 15 | const ProfileButton = () => { 16 | const { currentUser } = useAuthContext(); 17 | 18 | const onClick = async () => { 19 | if (currentUser) { 20 | return Events.emit('openprofilemodal'); 21 | } 22 | 23 | return Events.emit('opensigninmodal'); 24 | }; 25 | 26 | return ( 27 | 45 | ); 46 | }; 47 | export { ProfileButton }; 48 | -------------------------------------------------------------------------------- /src/components/components/ProfileButton/ProfileButton.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .profileButton { 4 | border: unset; 5 | border-radius: 50%; 6 | width: 43px; 7 | height: 43px; 8 | background-color: variables.$purple; 9 | padding: 0 !important; 10 | &:hover { 11 | background-color: variables.$purple-100; 12 | } 13 | &:active { 14 | background-color: variables.$purple-200; 15 | } 16 | } 17 | .photoURL { 18 | width: 43px; 19 | height: 43px; 20 | border-radius: 18px; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/components/ProfileButton/icons.jsx: -------------------------------------------------------------------------------- 1 | const Profile32Icon = ( 2 | 9 | 15 | 16 | ); 17 | 18 | export { Profile32Icon }; 19 | -------------------------------------------------------------------------------- /src/components/components/ProfileButton/index.js: -------------------------------------------------------------------------------- 1 | export { ProfileButton } from './ProfileButton.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/SceneCard/SceneCard.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .wrapper { 4 | display: flex; 5 | flex-wrap: wrap; 6 | justify-content: flex-start; 7 | row-gap: 24px; 8 | column-gap: 20px; 9 | padding-bottom: 20px; 10 | 11 | .img { 12 | margin-bottom: 6px; 13 | object-fit: cover; 14 | width: 272px; 15 | height: 204px; 16 | } 17 | 18 | .card { 19 | border-radius: 4px; 20 | border: 1px solid rgba(237, 235, 239, 0.2); 21 | padding: 8px 8px 12px 8px; 22 | width: 272px; 23 | height: 292px; 24 | position: relative; 25 | cursor: pointer; 26 | 27 | &:hover { 28 | border: 1px solid variables.$purple-100; 29 | transition: ease-out 0.3s; 30 | } 31 | 32 | &:active { 33 | border-color: variables.$purple-200; 34 | transition: all 0.3s; 35 | } 36 | 37 | &:disabled { 38 | border: 1px solid rgba(237, 235, 239, 0.5); 39 | transition: all 0.3s; 40 | } 41 | } 42 | 43 | .menuBlock { 44 | display: flex; 45 | flex-direction: column; 46 | width: 189px; 47 | border-radius: 8px; 48 | border: 1px solid variables.$purple-700; 49 | position: absolute; 50 | height: 70px; 51 | width: 190px; 52 | background-color: variables.$darkgray-300; 53 | padding: 6px 0px; 54 | top: 140px; 55 | right: -75px; 56 | z-index: 10; 57 | 58 | .menuItem { 59 | font-size: 16px; 60 | padding: 8px 8px 8px 20px; 61 | 62 | &:hover { 63 | background-color: variables.$black-400; 64 | transition: all 0.3s; 65 | } 66 | 67 | &:active { 68 | background-color: variables.$black-700; 69 | color: variables.$lightgray-200; 70 | transition: all 0.3s; 71 | } 72 | 73 | &:disabled { 74 | background-color: rgba(50, 50, 50, 0.6); 75 | color: variables.$gray-500; 76 | transition: all 0.3s; 77 | } 78 | } 79 | } 80 | 81 | .userBlock { 82 | display: flex; 83 | align-items: center; 84 | justify-content: space-between; 85 | 86 | .dropdown { 87 | width: 20px; 88 | padding: 0px; 89 | background: none; 90 | border-radius: 0; 91 | } 92 | 93 | .userName { 94 | display: flex; 95 | align-items: center; 96 | column-gap: 8px; 97 | } 98 | } 99 | 100 | .editButtons { 101 | display: flex; 102 | justify-content: space-between; 103 | align-items: center; 104 | column-gap: 8px; 105 | 106 | .editButton { 107 | width: 100%; 108 | border-radius: 10px; 109 | } 110 | } 111 | 112 | .editInput { 113 | font-size: 16px !important; 114 | text-overflow: ellipsis; 115 | overflow: hidden; 116 | white-space: nowrap; 117 | margin-bottom: 12px; 118 | width: 100%; 119 | } 120 | 121 | .title { 122 | font-size: 16px !important; 123 | text-overflow: ellipsis; 124 | overflow: hidden; 125 | white-space: nowrap; 126 | margin-bottom: 12px; 127 | } 128 | 129 | .date { 130 | margin-top: 4px; 131 | color: rgb(116, 116, 116); 132 | text-overflow: ellipsis; 133 | overflow: hidden; 134 | white-space: nowrap; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/components/components/SceneCard/index.js: -------------------------------------------------------------------------------- 1 | export { SceneCard } from './SceneCard.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/SceneEditTitle/SceneEditTitle.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styles from './SceneEditTitle.module.scss'; 3 | import { CheckMark32Icon, Cross32Icon, Edit32Icon } from '../../../icons'; 4 | import { updateSceneIdAndTitle } from '../../../api/scene'; 5 | 6 | const SceneEditTitle = ({ sceneData }) => { 7 | const [editMode, setEditMode] = useState(false); 8 | const [title, setTitle] = useState(sceneData?.sceneTitle); 9 | 10 | const sceneId = STREET.utils.getCurrentSceneId(); 11 | 12 | useEffect(() => { 13 | if (sceneData.sceneId === sceneId) { 14 | setTitle(sceneData.sceneTitle); 15 | } 16 | }, [sceneData?.sceneTitle, sceneData?.sceneId, sceneId]); 17 | 18 | const handleEditClick = () => { 19 | const newTitle = prompt('Edit the title:', title); 20 | 21 | if (newTitle !== null) { 22 | if (newTitle !== title) { 23 | setTitle(newTitle); 24 | saveNewTitle(newTitle); 25 | } 26 | } 27 | }; 28 | 29 | const saveNewTitle = async (newTitle) => { 30 | setEditMode(false); 31 | try { 32 | await updateSceneIdAndTitle(sceneData?.sceneId, newTitle); 33 | AFRAME.scenes[0].setAttribute('metadata', 'sceneTitle', newTitle); 34 | AFRAME.scenes[0].setAttribute('metadata', 'sceneId', sceneData?.sceneId); 35 | STREET.notify.successMessage(`New scene title saved: ${newTitle}`); 36 | } catch (error) { 37 | console.error('Error with update title', error); 38 | STREET.notify.errorMessage(`Error updating scene title: ${error}`); 39 | } 40 | }; 41 | 42 | const handleCancelClick = () => { 43 | if (sceneData && sceneData.sceneTitle !== undefined) { 44 | setTitle(sceneData.sceneTitle); 45 | } 46 | setEditMode(false); 47 | }; 48 | 49 | const handleKeyDown = (event) => { 50 | if (event.key === 'Enter') { 51 | saveNewTitle(); 52 | } else if (event.key === 'Escape') { 53 | handleCancelClick(); 54 | } 55 | }; 56 | return ( 57 |
58 | {editMode ? ( 59 |
60 | 66 |
67 |
saveNewTitle(title)} className={styles.check}> 68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 | ) : ( 76 |
77 |

78 | {title} 79 |

80 | {!editMode && ( 81 |
82 | 83 |
84 | )} 85 |
86 | )} 87 |
88 | ); 89 | }; 90 | 91 | export { SceneEditTitle }; 92 | -------------------------------------------------------------------------------- /src/components/components/SceneEditTitle/SceneEditTitle.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .wrapper { 4 | position: absolute; 5 | left: 110px; 6 | bottom: 34px; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | .title { 12 | width: 300px; 13 | text-overflow: ellipsis; 14 | overflow: hidden; 15 | height: 24px; 16 | font-size: 20px !important; 17 | white-space: nowrap; 18 | cursor: pointer; 19 | } 20 | 21 | .edit { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | cursor: pointer; 26 | } 27 | 28 | .buttons { 29 | display: flex; 30 | row-gap: 8px; 31 | cursor: pointer; 32 | transition: filter 0.2s, transform 0.2s; 33 | 34 | .check { 35 | filter: brightness(90%); 36 | &:hover { 37 | filter: brightness(100%); 38 | } 39 | 40 | &:focus { 41 | outline: none; 42 | } 43 | 44 | &:active { 45 | filter: brightness(90%); 46 | box-shadow: none; 47 | } 48 | } 49 | 50 | .cross { 51 | filter: brightness(90%); 52 | &:hover { 53 | filter: brightness(100%); 54 | } 55 | 56 | &:focus { 57 | outline: none; 58 | } 59 | 60 | &:active { 61 | filter: brightness(90%); 62 | box-shadow: none; 63 | } 64 | } 65 | 66 | svg { 67 | width: 32px; 68 | height: 32px; 69 | } 70 | } 71 | 72 | .readOnly { 73 | display: flex; 74 | justify-content: center; 75 | align-items: center; 76 | cursor: pointer; 77 | transition: filter 0.2s, transform 0.2s; 78 | filter: brightness(90%); 79 | 80 | &:hover { 81 | filter: brightness(100%); 82 | } 83 | 84 | &:focus { 85 | outline: none; 86 | } 87 | 88 | &:active { 89 | filter: brightness(90%); 90 | box-shadow: none; 91 | } 92 | } 93 | } 94 | 95 | input { 96 | background: none !important; 97 | color: variables.$white !important; 98 | font-size: 20px; 99 | user-select: text; 100 | } 101 | 102 | input::selection { 103 | background-color: variables.$purple; 104 | opacity: 0.4; 105 | color: variables.$white; 106 | } 107 | -------------------------------------------------------------------------------- /src/components/components/SceneEditTitle/index.js: -------------------------------------------------------------------------------- 1 | export { SceneEditTitle } from './SceneEditTitle.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/ScreenshotButton/ScreenshotButton.component.jsx: -------------------------------------------------------------------------------- 1 | import styles from './ScreenshotButton.module.scss'; 2 | 3 | import { Button } from '../Button'; 4 | import { Component } from 'react'; 5 | import Events from '../../../lib/Events.js'; 6 | import { ScreenshotIcon } from './icons.jsx'; 7 | 8 | /** 9 | * ScreenshotButton component. 10 | * 11 | * @author Ihor Dubas 12 | * @category Components. 13 | */ 14 | class ScreenshotButton extends Component { 15 | render() { 16 | const onClick = () => Events.emit('openscreenshotmodal'); 17 | return ( 18 | 28 | ); 29 | } 30 | } 31 | export { ScreenshotButton }; 32 | -------------------------------------------------------------------------------- /src/components/components/ScreenshotButton/ScreenshotButton.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .screenshotButton { 4 | border: unset; 5 | 6 | background-color: variables.$purple; 7 | &:hover { 8 | background-color: variables.$purple-100; 9 | } 10 | &:active { 11 | background-color: variables.$purple-200; 12 | } 13 | } 14 | 15 | @media screen and (max-width: 1024px) { 16 | .innerText { 17 | display: none !important; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/components/ScreenshotButton/icons.jsx: -------------------------------------------------------------------------------- 1 | const ScreenshotIcon = ( 2 | 9 | 15 | 21 | 25 | 26 | ); 27 | export { ScreenshotIcon }; 28 | -------------------------------------------------------------------------------- /src/components/components/ScreenshotButton/index.js: -------------------------------------------------------------------------------- 1 | export { ScreenshotButton } from './ScreenshotButton.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/Tabs/Tabs.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { arrayOf, bool, func, shape, string } from 'prop-types'; 4 | import styles from './Tabs.module.scss'; 5 | import { Hint } from './components'; 6 | 7 | /** 8 | * Tabs component. 9 | * 10 | * @author Oleksii Medvediev 11 | * @category Components. 12 | * @param {{ 13 | * tabs?: { 14 | * isSelected: boolean; 15 | * onClick: (value: string) => void; 16 | * label: string; 17 | * value: string; 18 | * hint?: string; 19 | * disabled?: boolean 20 | * }; 21 | * selectedTabClassName: string; 22 | * }} props 23 | */ 24 | const Tabs = ({ tabs, selectedTabClassName, className }) => ( 25 |
26 | {!!tabs?.length && 27 | tabs.map(({ label, value, onClick, isSelected, hint, disabled }) => ( 28 | 42 | ))} 43 |
44 | ); 45 | 46 | Tabs.propTypes = { 47 | tabs: arrayOf( 48 | shape({ 49 | isSelected: bool.isRequired, 50 | onClick: func.isRequired, 51 | label: string.isRequired, 52 | value: string.isRequired, 53 | hint: string 54 | }) 55 | ), 56 | selectedTabClassName: string 57 | }; 58 | 59 | export { Tabs }; 60 | -------------------------------------------------------------------------------- /src/components/components/Tabs/Tabs.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .wrapper { 4 | display: flex; 5 | align-items: center; 6 | width: fit-content; 7 | column-gap: 12px; 8 | margin: 14px auto 0; 9 | background: rgba(55, 55, 55, 0.4); 10 | border-radius: 16px; 11 | padding: 8px 12px; 12 | 13 | .activeTab, 14 | .inactiveTab { 15 | display: block; 16 | position: relative; 17 | border: unset; 18 | outline: unset; 19 | background-color: transparent; 20 | 21 | margin: 0; 22 | padding: 8.5px 12px; 23 | border-radius: 11px; 24 | border: 1px solid transparent; 25 | 26 | font-size: 16px; 27 | line-height: 19.2px; 28 | font-weight: 400; 29 | 30 | transition: 0.3s ease-out; 31 | } 32 | 33 | .inactiveTab { 34 | color: variables.$white; 35 | 36 | &:hover { 37 | cursor: pointer; 38 | background-color: rgba($color: variables.$gray-500, $alpha: 0.4); 39 | } 40 | 41 | &:focus-visible { 42 | border-radius: 0; 43 | border: 1px solid variables.$white; 44 | } 45 | 46 | &:active { 47 | color: variables.$white; 48 | transition: all 0.3s; 49 | } 50 | 51 | &.disabled { 52 | color: variables.$gray-500; 53 | transition: all 0.3s; 54 | cursor: not-allowed; 55 | } 56 | } 57 | 58 | .activeTab { 59 | color: variables.$white; 60 | background-color: variables.$gray-500 !important; 61 | 62 | &:focus-visible { 63 | border-radius: 11px; 64 | border: 1px solid variables.$white; 65 | background-color: variables.$black-400; 66 | } 67 | 68 | &:active { 69 | background-color: variables.$black-700; 70 | transition: all 0.3s; 71 | } 72 | 73 | &.disabled { 74 | background-color: rgba(50, 50, 50, 0.6); 75 | color: variables.$gray-500; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/components/Tabs/components/Hint/Hint.component.jsx: -------------------------------------------------------------------------------- 1 | import { string } from 'prop-types'; 2 | import './Hint.scss'; 3 | import { useEffect } from 'react'; 4 | 5 | /** 6 | * Hint component. 7 | * Exclusively for the Tabs component's tab button. 8 | * 9 | * @author Oleksii Medvediev 10 | * @category Components. 11 | * @param {{ 12 | * hint: string; 13 | * tab: string; 14 | * }} props 15 | */ 16 | const Hint = ({ hint, tab }) => { 17 | useEffect(() => { 18 | const hintElement = document?.getElementById(tab.concat('Tab')); 19 | 20 | hintElement && 21 | !hintElement.hasAttribute('style') && 22 | hintElement.setAttribute( 23 | 'style', 24 | `left: calc(50% - ${hintElement.clientWidth / 2}px)` 25 | ); 26 | }, [document]); 27 | 28 | return ( 29 |
30 | {hint} 31 |
32 | ); 33 | }; 34 | 35 | Hint.propTypes = { 36 | hint: string.isRequired, 37 | tab: string.isRequired 38 | }; 39 | 40 | export { Hint }; 41 | -------------------------------------------------------------------------------- /src/components/components/Tabs/components/Hint/Hint.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../../style/variables.scss'; 2 | 3 | // need to be global for working with Tab component 4 | 5 | .wrapper { 6 | position: absolute; 7 | top: 50px; 8 | min-width: 150px; 9 | max-width: 245px; 10 | display: flex; 11 | visibility: hidden; 12 | opacity: 0; 13 | justify-content: flex-start; 14 | align-items: flex-start; 15 | transition: all 0.3s; 16 | background-color: rgba(55, 55, 55, 0.8); 17 | padding: 6px 8px; 18 | border-radius: 8px; 19 | 20 | span { 21 | position: relative; 22 | display: flex; 23 | width: 100%; 24 | font-size: 12px; 25 | line-height: 14.4px; 26 | font-weight: 500; 27 | color: variables.$white; 28 | 29 | &::before { 30 | position: absolute; 31 | top: -12px; 32 | left: calc(50% - 3px); 33 | display: block; 34 | content: ''; 35 | width: 12px; 36 | height: 6px; 37 | background-color: rgba(55, 55, 55, 0.8); 38 | clip-path: polygon(50% 0%, 0% 100%, 100% 100%); 39 | } 40 | } 41 | 42 | &.hintVisible { 43 | visibility: visible; 44 | opacity: 1; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/components/Tabs/components/Hint/index.js: -------------------------------------------------------------------------------- 1 | export { Hint } from './Hint.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/Tabs/components/index.js: -------------------------------------------------------------------------------- 1 | export { Hint } from './Hint'; 2 | -------------------------------------------------------------------------------- /src/components/components/Tabs/index.js: -------------------------------------------------------------------------------- 1 | export { Tabs } from './Tabs.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/Toggle/Toggle.component.jsx: -------------------------------------------------------------------------------- 1 | import { bool, func, shape, string } from 'prop-types'; 2 | 3 | import classNames from 'classnames'; 4 | import styles from './Toggle.module.scss'; 5 | import { v4 } from 'uuid'; 6 | 7 | const toggleId = v4(); 8 | 9 | /** 10 | * Toggle component. 11 | * 12 | * @author Oleksii Medvediev 13 | * @category Components 14 | * @param {{ 15 | * status: boolean; 16 | * onChange: (value: boolean) => void; 17 | * disabled?: boolean; 18 | * className?: string; 19 | * label?: { 20 | * text: string; 21 | * position?: 'left' | 'right' 22 | * }; 23 | * id?: string; 24 | * }} props 25 | * @returns 26 | */ 27 | const Toggle = ({ 28 | status, 29 | onChange, 30 | disabled, 31 | className, 32 | label, 33 | id = toggleId 34 | }) => ( 35 |
36 | {label && label.position && label.position === 'left' && ( 37 |
40 | {label.text} 41 |
42 | )} 43 |
44 | onChange(!status)} 48 | disabled={disabled} 49 | type={'checkbox'} 50 | checked={status} 51 | /> 52 | 63 |
64 | {label && label.position && label.position === 'right' && ( 65 |
68 | {label.text} 69 |
70 | )} 71 |
72 | ); 73 | 74 | Toggle.propTypes = { 75 | status: bool.isRequired, 76 | onChange: func.isRequired, 77 | disabled: bool, 78 | className: string, 79 | label: shape({ 80 | text: string.isRequired, 81 | position: 'left' || 'right' 82 | }), 83 | id: string 84 | }; 85 | 86 | export { Toggle }; 87 | -------------------------------------------------------------------------------- /src/components/components/Toggle/Toggle.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .wrapper { 4 | display: flex; 5 | align-items: center; 6 | column-gap: 0.75rem; 7 | width: fit-content; 8 | 9 | .label { 10 | display: flex; 11 | font-size: 1rem; 12 | line-height: 1.1875rem; 13 | font-weight: 400; 14 | color: variables.$white; 15 | transition: all 0.3s; 16 | } 17 | 18 | .disabledLabel { 19 | transition: all 0.3s; 20 | color: variables.$gray-700; 21 | } 22 | 23 | .toggleContainer { 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | width: 40px; 28 | min-width: 40px; 29 | height: 40px; 30 | min-height: 40px; 31 | 32 | .toggle { 33 | position: relative; 34 | display: flex; 35 | width: 100%; 36 | height: 20px; 37 | border: 1px solid variables.$lightgray-400; 38 | border-radius: 10px; 39 | box-sizing: border-box; 40 | 41 | .switcher { 42 | position: absolute; 43 | top: 0; 44 | bottom: 0; 45 | left: 0; 46 | margin: auto 0; 47 | display: flex; 48 | width: 13.33px; 49 | height: 13.33px; 50 | border-radius: 50%; 51 | } 52 | 53 | &:hover { 54 | cursor: pointer; 55 | } 56 | } 57 | 58 | .activeToggle { 59 | background-color: variables.$lightgray-400; 60 | transition: all 0.3s; 61 | 62 | .switcher { 63 | transform: translateX(22.33px); 64 | transition: all 0.3s; 65 | background-color: variables.$darkpurple-100; 66 | } 67 | 68 | &:hover { 69 | transition: all 0.3s; 70 | background-color: variables.$white; 71 | border-color: variables.$white; 72 | } 73 | 74 | &:active { 75 | transition: all 0.3s; 76 | background-color: #00000020; 77 | border-color: #00000020; 78 | } 79 | } 80 | 81 | .inactiveToggle { 82 | background-color: transparent; 83 | transition: all 0.3s; 84 | 85 | .switcher { 86 | transform: translateX(3.33px); 87 | transition: all 0.3s; 88 | background-color: variables.$lightgray-400; 89 | } 90 | 91 | &:hover { 92 | transition: all 0.3s; 93 | border-color: variables.$white; 94 | 95 | .switcher { 96 | transition: all 0.3s; 97 | background-color: variables.$white; 98 | } 99 | } 100 | 101 | &:active { 102 | transition: all 0.3s; 103 | border-color: #00000020; 104 | 105 | .switcher { 106 | transition: all 0.3s; 107 | background-color: #00000020; 108 | } 109 | } 110 | } 111 | 112 | .disabledActiveToggle, 113 | .disabledActiveToggle:hover, 114 | .disabledActiveToggle:active { 115 | transition: all 0.3s; 116 | background-color: variables.$gray-700; 117 | border-color: variables.$gray-700; 118 | 119 | .switcher { 120 | transition: all 0.3s; 121 | background-color: variables.$black-800; 122 | } 123 | 124 | &:hover { 125 | cursor: not-allowed; 126 | } 127 | } 128 | 129 | .disabledInactiveToggle, 130 | .disabledInactiveToggle:hover, 131 | .disabledInactiveToggle:active { 132 | transition: all 0.3s; 133 | background-color: transparent; 134 | border-color: variables.$gray-700; 135 | 136 | .switcher { 137 | transition: all 0.3s; 138 | background-color: variables.$gray-700; 139 | } 140 | 141 | &:hover { 142 | cursor: not-allowed; 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/components/components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | export { Toggle } from './Toggle.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/ZoomButtons/ZoomButtons.component.jsx: -------------------------------------------------------------------------------- 1 | import styles from './ZoomButtons.module.scss'; 2 | 3 | import { Button } from '../Button'; 4 | import { Component } from 'react'; 5 | import classNames from 'classnames'; 6 | 7 | /** 8 | * ZoomButtons component. 9 | * 10 | * @author Oleksii Medvediev 11 | * @category Components 12 | */ 13 | class ZoomButtons extends Component { 14 | render() { 15 | return ( 16 |
17 |
30 | ); 31 | } 32 | } 33 | 34 | export { ZoomButtons }; 35 | -------------------------------------------------------------------------------- /src/components/components/ZoomButtons/ZoomButtons.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .wrapper { 4 | position: absolute; 5 | z-index: 1; 6 | top: unset; 7 | right: 32px; 8 | bottom: 74px; 9 | display: block; 10 | min-width: unset; 11 | width: 3rem; 12 | height: 6rem; 13 | overflow: hidden; 14 | border-radius: 32px; 15 | margin: 0; 16 | padding: 0; 17 | opacity: 1; 18 | visibility: visible; 19 | .btn { 20 | display: block; 21 | width: 100%; 22 | height: 50%; 23 | padding: 0; 24 | margin: 0; 25 | background-color: variables.$darkgray-300; 26 | outline: unset; 27 | border: unset; 28 | border-radius: unset; 29 | color: variables.$white; 30 | transition: all 0.3s; 31 | &.plusButton { 32 | background-image: url(variables.$plusButton); 33 | background-repeat: no-repeat; 34 | background-position: bottom 8px center; 35 | } 36 | &.minusButton { 37 | background-image: url(variables.$minusButton); 38 | background-repeat: no-repeat; 39 | background-position: top 8px center; 40 | } 41 | &:hover { 42 | background-color: variables.$purple-100; 43 | } 44 | &:active { 45 | background-color: variables.$purple-200; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/components/ZoomButtons/index.js: -------------------------------------------------------------------------------- 1 | export { ZoomButtons } from './ZoomButtons.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/components/index.js: -------------------------------------------------------------------------------- 1 | export { Tabs } from './Tabs'; 2 | export { HelpButton } from './HelpButton'; 3 | export { ZoomButtons } from './ZoomButtons'; 4 | export { Button } from './Button'; 5 | export { Input } from './Input'; 6 | export { Dropdown } from './Dropdown'; 7 | export { Checkbox } from './Checkbox'; 8 | export { Toggle } from './Toggle'; 9 | export { Logo } from './Logo'; 10 | export { ScreenshotButton } from './ScreenshotButton'; 11 | export { ProfileButton } from './ProfileButton'; 12 | export { SceneCard } from './SceneCard'; 13 | export { SceneEditTitle } from './SceneEditTitle'; 14 | -------------------------------------------------------------------------------- /src/components/modals/Modal.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../style/variables.scss'; 2 | 3 | .close { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | position: absolute; 8 | top: 1.3438rem; 9 | right: 1.3438rem; 10 | width: 32px; 11 | height: 32px; 12 | &:hover { 13 | border-radius: 50%; 14 | cursor: pointer; 15 | background-color: rgba(50, 50, 50, 0.6); 16 | transition: all 0.3s; 17 | } 18 | &:active { 19 | background-color: variables.$black-600; 20 | span { 21 | background-color: variables.$lightgray-200; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/ModalHelp.component.jsx: -------------------------------------------------------------------------------- 1 | import styles from './ModalHelp.module.scss'; 2 | 3 | import { EssentialActions, Shortcuts } from './components/index.js'; 4 | import { Component } from 'react'; 5 | 6 | import Modal from '../Modal.jsx'; 7 | import PropTypes from 'prop-types'; 8 | import { Tabs } from '../../components'; 9 | 10 | const tabs = [ 11 | { 12 | label: 'Essential Actions', 13 | value: 'essentialActions' 14 | }, 15 | { 16 | label: 'Keyboard Shortcuts', 17 | value: 'shortcuts' 18 | } 19 | ]; 20 | 21 | class ModalHelp extends Component { 22 | static propTypes = { 23 | isOpen: PropTypes.bool, 24 | onClose: PropTypes.func.isRequired 25 | }; 26 | 27 | state = { 28 | selectedTab: 'essentialActions' 29 | }; 30 | 31 | handleChangeTab = (tab) => 32 | this.setState((prevState) => ({ 33 | ...prevState, 34 | selectedTab: tab 35 | })); 36 | 37 | render() { 38 | const { isOpen, onClose } = this.props; 39 | 40 | return ( 41 | ({ 46 | ...tab, 47 | isSelected: this.state.selectedTab === tab.value, 48 | onClick: () => this.handleChangeTab(tab.value) 49 | }))} 50 | selectedTabClassName={'selectedTab'} 51 | /> 52 | } 53 | isOpen={isOpen} 54 | onClose={onClose} 55 | extraCloseKeyCode={72} 56 | > 57 | {this.state.selectedTab === 'shortcuts' ? ( 58 | 59 | ) : ( 60 | 61 | )} 62 | 63 | ); 64 | } 65 | } 66 | 67 | export { ModalHelp }; 68 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/ModalHelp.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .helpModalWrapper { 4 | position: absolute; 5 | top: 115px; 6 | left: 0; 7 | right: 0; 8 | width: fit-content; 9 | } 10 | .help-lists { 11 | display: grid; 12 | width: 100%; 13 | grid-template-columns: 50% 50%; 14 | max-width: 782px; 15 | max-height: 50vh; 16 | overflow-y: auto; 17 | } 18 | .help-list { 19 | display: flex; 20 | flex-direction: column; 21 | row-gap: 12px; 22 | width: 100%; 23 | list-style: none; 24 | margin: 0; 25 | padding: 0; 26 | &:first-child { 27 | padding-right: 60px; 28 | width: calc(100% - 60px); 29 | } 30 | } 31 | .help-key-unit { 32 | display: flex; 33 | width: max-content; 34 | max-width: 100%; 35 | align-items: center; 36 | column-gap: 12px; 37 | } 38 | .help-key { 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | height: 32px; 43 | border: 1px solid variables.$white; 44 | border-radius: 16px; 45 | span { 46 | padding: 9px 11px; 47 | font-size: 14px; 48 | line-height: 14px; 49 | font-weight: 700; 50 | white-space: nowrap; 51 | } 52 | } 53 | .help-key-def { 54 | color: variables.$white; 55 | font-size: 1rem; 56 | line-height: 1.2rem; 57 | font-weight: 400; 58 | } 59 | .selectedTab { 60 | background-color: variables.$gray-500 !important; 61 | } 62 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/DocumentationButton/DocumentationButton.component.jsx: -------------------------------------------------------------------------------- 1 | import styles from './DocumentationButton.module.scss'; 2 | import { Button } from '../../../../components'; 3 | import { Component } from 'react'; 4 | import { Open } from './icons.jsx'; 5 | /** 6 | * DocumentationButton component. 7 | * Exclusively for the EssentialsActions and Shortcuts components. 8 | * 9 | * @author Ihor Dubas 10 | * @category Components. 11 | */ 12 | class DocumentationButton extends Component { 13 | render() { 14 | return ( 15 | 23 | ); 24 | } 25 | } 26 | export { DocumentationButton }; 27 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/DocumentationButton/DocumentationButton.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../../style/variables.scss'; 2 | 3 | .docsButtonWrapper { 4 | display: flex; 5 | align-items: center; 6 | column-gap: 16px; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/DocumentationButton/icons.jsx: -------------------------------------------------------------------------------- 1 | const Open = () => ( 2 | 9 | 16 | 17 | ); 18 | export { Open }; 19 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/DocumentationButton/index.js: -------------------------------------------------------------------------------- 1 | export { DocumentationButton } from './DocumentationButton.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/EssentialActions/EssentialActions.component.jsx: -------------------------------------------------------------------------------- 1 | import styles from './EssentialActions.module.scss'; 2 | 3 | import { 4 | Angle, 5 | Drag, 6 | Edit, 7 | RButton, 8 | Scroll, 9 | View, 10 | ZoomIn, 11 | ZoomOut 12 | } from './icons.jsx'; 13 | import { Component } from 'react'; 14 | import { DocumentationButton } from '../DocumentationButton'; 15 | 16 | const actions = [ 17 | { 18 | title: 'Move the map by dragging', 19 | description: 'Click and drag to pan the map view.', 20 | items: [[Angle, Drag]] 21 | }, 22 | { 23 | title: 'Zoom the map by scrolling', 24 | description: 25 | 'Use the mouse scrollwheel (or touchpad scrolling motion) to zoom in and out.', 26 | items: [[Scroll, 'or', ZoomOut, ZoomIn]] 27 | }, 28 | { 29 | title: 'Rotate the map by right-clicking and dragging', 30 | description: 31 | 'Right-click and drag to rotate the map while staying in place.', 32 | items: [[RButton, Drag]] 33 | }, 34 | { 35 | title: 'Mode switch', 36 | description: 37 | 'To switch between the "View" and "Edit" modes, click the button in the upper right corner.', 38 | items: [[View, Edit]] 39 | } 40 | ]; 41 | 42 | /** 43 | * EssentialActions component. 44 | * Exclusively for the HelpModal component as an 'Essential Actions' tab content. 45 | * 46 | * @author Oleksii Medvediev 47 | * @category Components. 48 | */ 49 | class EssentialActions extends Component { 50 | render() { 51 | return ( 52 |
53 | {actions.map(({ title, description, items }) => ( 54 |
55 |
56 |

{title}

57 |

{description}

58 |
59 |
60 | {items.map((row, index) => ( 61 |
65 | {row.map((item, index) => ( 66 | 67 | {item} 68 | 69 | ))} 70 |
71 | ))} 72 |
73 |
74 | ))} 75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | export { EssentialActions }; 82 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/EssentialActions/EssentialActions.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../../style/variables.scss'; 2 | 3 | .essentialActionsWrapper { 4 | display: flex; 5 | flex-flow: row wrap; 6 | row-gap: 2.5rem; 7 | column-gap: 2rem; 8 | width: 100%; 9 | max-width: 698px; 10 | padding: 0px 14px 0 4px; 11 | .action { 12 | display: flex; 13 | flex-direction: column; 14 | row-gap: 20px; 15 | width: calc(50% - 1rem); 16 | h3, 17 | p { 18 | margin: 0 !important; 19 | } 20 | .text { 21 | display: flex; 22 | flex-direction: column; 23 | row-gap: 12px; 24 | width: 100%; 25 | h3.actionTitle { 26 | font-size: 1rem; 27 | line-height: 1.1875rem; 28 | font-weight: 600; 29 | } 30 | p.actionDescription { 31 | font-size: 0.75rem; 32 | line-height: 0.9rem; 33 | font-weight: 500; 34 | } 35 | } 36 | .icons { 37 | display: flex; 38 | flex-direction: column; 39 | row-gap: 1.25rem; 40 | width: 100%; 41 | .itemsRow { 42 | display: flex; 43 | column-gap: 0.75rem; 44 | row-gap: 0.75rem; 45 | align-items: center; 46 | width: 100%; 47 | span { 48 | font-size: 1rem; 49 | line-height: 1.2rem; 50 | font-weight: 400; 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/EssentialActions/index.js: -------------------------------------------------------------------------------- 1 | export { EssentialActions } from './EssentialActions.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/Shortcuts/Shortcuts.component.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import styles from './Shortcuts.module.scss'; 3 | import { DocumentationButton } from '../DocumentationButton'; 4 | 5 | const shortcuts = [ 6 | [ 7 | { key: ['w'], description: 'Translate' }, 8 | { key: ['e'], description: 'Rotate' }, 9 | { key: ['r'], description: 'Scale' }, 10 | { key: ['d'], description: 'Duplicate selected entity' }, 11 | { key: ['f'], description: 'Focus on selected entity' }, 12 | { key: ['g'], description: 'Toggle grid visibility' }, 13 | { key: ['n'], description: 'Add new entity' }, 14 | { key: ['o'], description: 'Toggle local between global transform' }, 15 | { key: ['del | backspace'], description: 'Delete selected entity' } 16 | ], 17 | [ 18 | { key: ['0'], description: 'Toggle panels' }, 19 | { key: ['1'], description: 'Perspective view' }, 20 | { key: ['2'], description: 'Left view' }, 21 | { key: ['3'], description: 'Right view' }, 22 | { key: ['4'], description: 'Top view' }, 23 | { key: ['5'], description: 'Bottom view' }, 24 | { key: ['6'], description: 'Back view' }, 25 | { key: ['7'], description: 'Front view' }, 26 | 27 | { key: ['ctrl | cmd', 'x'], description: 'Cut selected entity' }, 28 | { key: ['ctrl | cmd', 'c'], description: 'Copy selected entity' }, 29 | { key: ['ctrl | cmd', 'v'], description: 'Paste entity' }, 30 | { key: ['h'], description: 'Show this help' }, 31 | { key: ['Esc'], description: 'Unselect entity' }, 32 | { key: ['ctrl', 'alt', 'i'], description: 'Switch Edit and VR Modes' } 33 | ] 34 | ]; 35 | 36 | /** 37 | * Shortcuts component. 38 | * Exclusively for the ModalHelp component as a 'Shortcuts' tab content. 39 | * 40 | * @author Oleksii Medvediev 41 | * @category Components. 42 | */ 43 | class Shortcuts extends Component { 44 | render() { 45 | return ( 46 |
47 | {shortcuts.map((column, idx) => ( 48 |
    49 | {column.map(({ description, key }) => ( 50 |
  • 51 | {key.map((item) => ( 52 | 53 | {item} 54 | 55 | ))} 56 | {description} 57 |
  • 58 | ))} 59 |
60 | ))} 61 | 62 |
63 | ); 64 | } 65 | } 66 | 67 | export { Shortcuts }; 68 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/Shortcuts/Shortcuts.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../../style/variables.scss'; 2 | 3 | .helpLists { 4 | display: grid; 5 | width: 100%; 6 | grid-template-columns: 50% 50%; 7 | max-width: 782px; 8 | .helpList { 9 | display: flex; 10 | flex-direction: column; 11 | row-gap: 12px; 12 | width: 100%; 13 | list-style: none; 14 | margin: 0; 15 | padding: 0; 16 | &:first-child { 17 | padding-right: 60px; 18 | width: calc(100% - 60px); 19 | } 20 | .helpKeyUnit { 21 | display: flex; 22 | width: max-content; 23 | max-width: 100%; 24 | align-items: center; 25 | column-gap: 12px; 26 | .helpKey { 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | height: 32px; 31 | border: 1px solid variables.$white; 32 | border-radius: 16px; 33 | span { 34 | padding: 9px 11px; 35 | font-size: 14px; 36 | line-height: 14px; 37 | font-weight: 700; 38 | white-space: nowrap; 39 | } 40 | } 41 | .helpKeyDef { 42 | color: variables.$white; 43 | font-size: 1rem; 44 | line-height: 1.2rem; 45 | font-weight: 400; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/Shortcuts/index.js: -------------------------------------------------------------------------------- 1 | export { Shortcuts } from './Shortcuts.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/components/index.js: -------------------------------------------------------------------------------- 1 | export { Shortcuts } from './Shortcuts'; 2 | export { EssentialActions } from './EssentialActions'; 3 | -------------------------------------------------------------------------------- /src/components/modals/ModalHelp/index.js: -------------------------------------------------------------------------------- 1 | export { ModalHelp } from './ModalHelp.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ProfileModal/ProfileModal.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .modalWrapper { 4 | position: relative; 5 | margin: auto auto; 6 | } 7 | 8 | .contentWrapper { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | width: 100%; 13 | row-gap: 28px; 14 | padding: 0px 14px 0px 4px; 15 | 16 | .title { 17 | position: absolute; 18 | top: 22px; 19 | left: 40px; 20 | font-size: 24px !important; 21 | } 22 | 23 | .content { 24 | display: flex; 25 | flex-direction: column; 26 | row-gap: 24px; 27 | margin-top: 38px; 28 | width: 100%; 29 | 30 | .header { 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | column-gap: 80px; 35 | width: 100%; 36 | 37 | .profile { 38 | display: flex; 39 | column-gap: 16px; 40 | 41 | img { 42 | width: 100px; 43 | height: 100px; 44 | border-radius: 78px; 45 | } 46 | 47 | .credentials { 48 | display: flex; 49 | flex-direction: column; 50 | row-gap: 12px; 51 | justify-content: center; 52 | 53 | .name { 54 | font-size: 20px; 55 | font-weight: 400; 56 | } 57 | 58 | .email { 59 | font-size: 20px; 60 | font-weight: 400; 61 | color: variables.$lightgray-200; 62 | } 63 | } 64 | } 65 | 66 | .logOut { 67 | border-radius: 18px; 68 | background: none; 69 | color: variables.$purple !important; 70 | border: 1px solid variables.$purple !important; 71 | } 72 | } 73 | 74 | .scenesWrapper { 75 | display: flex; 76 | flex-direction: column; 77 | row-gap: 20px; 78 | 79 | .scenes { 80 | display: flex; 81 | flex-wrap: wrap; 82 | width: 100%; 83 | column-gap: 36px; 84 | row-gap: 16px; 85 | height: 400px; 86 | overflow: auto; 87 | 88 | .dropzone { 89 | display: flex; 90 | flex-direction: column; 91 | align-items: center; 92 | justify-content: center; 93 | width: 262px; 94 | height: 220px; 95 | 96 | border-radius: 2px; 97 | border: 1px dashed rgba(237, 235, 239, 0.2); 98 | background: rgba(50, 50, 50, 0.4); 99 | 100 | .icon { 101 | margin-bottom: 24px; 102 | } 103 | 104 | .main { 105 | margin-bottom: 20px; 106 | font-size: 16px; 107 | font-weight: 500; 108 | } 109 | 110 | .streetmix { 111 | margin-bottom: 16px; 112 | } 113 | 114 | .json { 115 | width: auto; 116 | } 117 | 118 | .json, 119 | .streetmix { 120 | color: variables.$purple; 121 | font-size: 16px; 122 | font-weight: 500; 123 | } 124 | } 125 | 126 | .scene { 127 | box-sizing: border-box; 128 | display: flex; 129 | flex-direction: column; 130 | width: 262px; 131 | height: 220px; 132 | padding: 8px 8px 12px 8px; 133 | border-radius: 4px; 134 | border: 1px solid rgba(237, 235, 239, 0.2); 135 | cursor: pointer; 136 | 137 | .img { 138 | width: 100%; 139 | height: 144px; 140 | margin-bottom: 12px; 141 | } 142 | 143 | .name { 144 | font-size: 16px; 145 | font-weight: 600; 146 | margin-bottom: 8px; 147 | } 148 | 149 | .date { 150 | color: variables.$lightgray-200; 151 | font-size: 14px; 152 | font-weight: 400; 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/components/modals/ProfileModal/icons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Download32Icon = ( 4 | 11 | 17 | 18 | ); 19 | 20 | export { Download32Icon }; 21 | -------------------------------------------------------------------------------- /src/components/modals/ProfileModal/index.js: -------------------------------------------------------------------------------- 1 | export { ProfileModal } from './ProfileModal.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/modals/SavingModal/SavingModal.component.jsx: -------------------------------------------------------------------------------- 1 | import styles from './SavingModal.module.scss'; 2 | 3 | import { Component } from 'react'; 4 | 5 | /** 6 | * SavingModal component. 7 | * 8 | * @author Oleksii Medvediev 9 | * @category Components 10 | */ 11 | class SavingModal extends Component { 12 | render() { 13 | return ( 14 |
15 |
16 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | Saving ... 38 |
39 | ); 40 | } 41 | } 42 | 43 | export { SavingModal }; 44 | -------------------------------------------------------------------------------- /src/components/modals/SavingModal/SavingModal.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .savingModalWrapper { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | z-index: 10; 8 | display: flex; 9 | flex-direction: column; 10 | row-gap: 20px; 11 | width: 100vw; 12 | height: 100vh; 13 | align-items: center; 14 | justify-content: center; 15 | background-color: rgba(0, 0, 0, 0.7); 16 | .preloaderBox { 17 | display: flex; 18 | max-width: 100px; 19 | max-height: 100px; 20 | padding: 20px; 21 | background-color: rgba(21, 21, 21, 0.7); 22 | border-radius: 50%; 23 | .preloader { 24 | animation: rotation 1s linear infinite; 25 | } 26 | } 27 | .action { 28 | font-size: 20px; 29 | line-height: 24px; 30 | font-weight: 400; 31 | color: variables.$white; 32 | } 33 | } 34 | @keyframes rotation { 35 | from { 36 | transform: rotateZ(0deg); 37 | } 38 | to { 39 | transform: rotateZ(359deg); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/modals/SavingModal/index.js: -------------------------------------------------------------------------------- 1 | export { SavingModal } from './SavingModal.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ScenesModal/ScenesModal.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .modalWrapper { 4 | position: relative; 5 | width: 90vw; 6 | margin: auto auto; 7 | height: 100%; 8 | 9 | .contentWrapper { 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: start; 13 | width: 100%; 14 | height: 100%; 15 | 16 | .loadMore { 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | 21 | .button { 22 | width: 200px; 23 | height: 48px; 24 | font-size: 18px !important; 25 | } 26 | } 27 | 28 | .loadingSpinner { 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | margin: auto; 33 | .spinner { 34 | animation: spin 2s linear infinite; 35 | } 36 | 37 | @keyframes spin { 38 | 0% { 39 | transform: rotate(0deg); 40 | } 41 | 100% { 42 | transform: rotate(360deg); 43 | } 44 | } 45 | } 46 | 47 | .signInFirst { 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | flex-direction: column; 52 | width: 100%; 53 | min-height: 100%; 54 | row-gap: 28px; 55 | height: calc(80vh - 240px); 56 | 57 | .title { 58 | font-size: 24px; 59 | font-weight: 500; 60 | } 61 | 62 | .buttons { 63 | display: flex; 64 | column-gap: 24px; 65 | } 66 | } 67 | 68 | .loadingSpinner { 69 | display: flex; 70 | justify-content: center; 71 | align-items: center; 72 | margin: auto; 73 | .spinner { 74 | animation: spin 2s linear infinite; 75 | } 76 | 77 | @keyframes spin { 78 | 0% { 79 | transform: rotate(0deg); 80 | } 81 | 100% { 82 | transform: rotate(360deg); 83 | } 84 | } 85 | } 86 | 87 | .loadMore { 88 | display: flex; 89 | justify-content: center; 90 | align-items: center; 91 | 92 | .button { 93 | width: 200px; 94 | height: 48px; 95 | font-size: 18px !important; 96 | } 97 | } 98 | } 99 | 100 | .header { 101 | display: flex; 102 | justify-content: space-between; 103 | align-items: center; 104 | margin: 32px 0px 0px; 105 | 106 | .tabs { 107 | margin: 0px; 108 | } 109 | 110 | .buttons { 111 | display: flex; 112 | justify-content: center; 113 | align-items: center; 114 | column-gap: 16px; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/modals/ScenesModal/index.js: -------------------------------------------------------------------------------- 1 | export { ScenesModal } from './ScenesModal.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ScreenshotModal/ScreenshotModal.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .screenshotModalWrapper { 4 | position: absolute; 5 | top: 115px; 6 | left: 0; 7 | right: 0; 8 | width: 80vw; 9 | height: 90vh; 10 | overflow-y: scroll; 11 | 12 | .wrapper { 13 | width: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | row-gap: 8px; 18 | 19 | .header { 20 | display: flex; 21 | justify-content: space-between; 22 | align-items: center; 23 | width: 100%; 24 | .forms { 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | width: 100%; 29 | 30 | .inputContainer { 31 | display: flex; 32 | align-items: center; 33 | width: 50%; 34 | 35 | .input { 36 | border: none; 37 | background: none; 38 | } 39 | 40 | .button { 41 | padding: 0px 10px 0px 0px !important; 42 | cursor: pointer; 43 | transition: filter 0.2s, transform 0.2s, box-shadow 0.2s; 44 | filter: brightness(85%); 45 | 46 | svg { 47 | width: 30px; 48 | height: 30px; 49 | } 50 | &:hover { 51 | filter: brightness(100%); 52 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | &:focus { 56 | outline: none; 57 | } 58 | 59 | &:active { 60 | filter: brightness(70%); 61 | box-shadow: none; 62 | } 63 | } 64 | } 65 | 66 | .dropdown { 67 | max-width: fit-content; 68 | } 69 | } 70 | } 71 | .imageWrapper { 72 | justify-content: center; 73 | display: flex; 74 | height: fit-content; 75 | position: relative; 76 | width: fit-content; 77 | max-height: calc(100% - 44px - 24px); 78 | 79 | .screenshotWrapper { 80 | height: fit-content; 81 | max-width: 100%; 82 | max-height: 100%; 83 | } 84 | 85 | .screenshotWrapper, 86 | .screenshotWrapper img { 87 | width: 100%; 88 | height: 100%; 89 | object-fit: contain; 90 | } 91 | 92 | .thumbnailButton { 93 | position: absolute !important; 94 | bottom: 16px; 95 | right: 16px; 96 | } 97 | } 98 | } 99 | } 100 | 101 | .inputContainer .input { 102 | height: 43px; 103 | border: none; 104 | background: none; 105 | padding: 0px; 106 | color: variables.$white; 107 | display: flex; 108 | justify-content: center; 109 | } 110 | -------------------------------------------------------------------------------- /src/components/modals/ScreenshotModal/index.js: -------------------------------------------------------------------------------- 1 | export { ScreenshotModal } from './ScreenshotModal.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/modals/SignInModal/SignInModal.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { GoogleSignInButtonSVG } from '../../../icons'; 3 | import Modal from '../Modal.jsx'; 4 | import styles from './SignInModal.module.scss'; 5 | import { signIn } from '../../../api'; 6 | 7 | const SignInModal = ({ isOpen, onClose }) => ( 8 | 14 |
15 |

Sign in to 3DStreet Cloud

16 |
17 |

18 | Save and share your street scenes with 3DStreet Cloud.{' '} 19 |

20 |

21 | 26 | This is beta software which may not work as expected.{' '} 27 | 28 |

29 |
30 |
{ 32 | signIn(); 33 | onClose(); 34 | }} 35 | alt="Sign In with Google Button" 36 | className={styles.signInButton} 37 | > 38 | 39 |
40 |
41 |
42 | ); 43 | 44 | export { SignInModal }; 45 | -------------------------------------------------------------------------------- /src/components/modals/SignInModal/SignInModal.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | .modalWrapper { 4 | position: relative; 5 | width: 476px; 6 | margin: auto auto; 7 | } 8 | 9 | .contentWrapper { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | row-gap: 28px; 14 | padding: 0px 14px 0px 4px; 15 | 16 | .title { 17 | position: absolute; 18 | top: 22px; 19 | left: 40px; 20 | font-size: 24px !important; 21 | } 22 | 23 | .content { 24 | display: flex; 25 | flex-direction: column; 26 | row-gap: 16px; 27 | margin-top: 38px; 28 | 29 | .p1 { 30 | font-size: 16px !important; 31 | font-weight: 600; 32 | 33 | a { 34 | color: variables.$purple; 35 | text-decoration: none; 36 | font-size: 16px !important; 37 | } 38 | } 39 | 40 | .p2 { 41 | color: variables.$lightgray-400; 42 | 43 | * { 44 | font-size: 14px; 45 | font-weight: 500; 46 | } 47 | 48 | a { 49 | color: variables.$purple; 50 | text-decoration: none; 51 | } 52 | } 53 | } 54 | 55 | .signInButton { 56 | cursor: pointer; 57 | background-color: transparent; 58 | border: none; 59 | cursor: pointer; 60 | transition: filter 0.2s, transform 0.2s, box-shadow 0.2s; 61 | 62 | &:hover { 63 | filter: brightness(85%); 64 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 65 | } 66 | 67 | &:focus { 68 | outline: none; 69 | } 70 | 71 | &:active { 72 | filter: brightness(70%); 73 | box-shadow: none; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/modals/SignInModal/index.js: -------------------------------------------------------------------------------- 1 | export { SignInModal } from './SignInModal.component.jsx'; 2 | -------------------------------------------------------------------------------- /src/components/scenegraph/Entity.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-danger */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import classNames from 'classnames'; 5 | import Events from '../../lib/Events'; 6 | import { printEntity, removeEntity, cloneEntity } from '../../lib/entity'; 7 | 8 | export default class Entity extends React.Component { 9 | static propTypes = { 10 | depth: PropTypes.number, 11 | entity: PropTypes.object, 12 | isExpanded: PropTypes.bool, 13 | isFiltering: PropTypes.bool, 14 | isSelected: PropTypes.bool, 15 | selectEntity: PropTypes.func, 16 | toggleExpandedCollapsed: PropTypes.func, 17 | isInitiallyExpanded: PropTypes.bool, 18 | initiallyExpandEntity: PropTypes.func 19 | }; 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = {}; 24 | } 25 | 26 | componentDidMount() { 27 | !this.props.isInitiallyExpanded && this.props.initiallyExpandEntity(); 28 | } 29 | 30 | onClick = (evt) => { 31 | if (!evt.target.classList.contains('fa')) { 32 | this.props.selectEntity(this.props.entity); 33 | } 34 | }; 35 | 36 | onDoubleClick = () => Events.emit('objectfocus', this.props.entity.object3D); 37 | 38 | toggleVisibility = (evt) => { 39 | const entity = this.props.entity; 40 | const visible = 41 | entity.tagName.toLowerCase() === 'a-scene' 42 | ? entity.object3D.visible 43 | : entity.getAttribute('visible'); 44 | entity.setAttribute('visible', !visible); 45 | // manually call render function 46 | this.forceUpdate(); 47 | }; 48 | 49 | render() { 50 | const isFiltering = this.props.isFiltering; 51 | const isExpanded = this.props.isExpanded; 52 | const entity = this.props.entity; 53 | const tagName = entity.tagName.toLowerCase(); 54 | 55 | // Clone and remove buttons if not a-scene. 56 | const cloneButton = 57 | tagName === 'a-scene' ? null : ( 58 | cloneEntity(entity)} 60 | title="Clone entity" 61 | className="button fa fa-clone" 62 | /> 63 | ); 64 | const removeButton = 65 | tagName === 'a-scene' ? null : ( 66 | { 68 | event.stopPropagation(); 69 | removeEntity(entity); 70 | }} 71 | title="Remove entity" 72 | className="button fa fa-trash" 73 | /> 74 | ); 75 | 76 | let collapse; 77 | if (entity.children.length > 0 && !isFiltering) { 78 | collapse = ( 79 | { 81 | evt.stopPropagation(); 82 | this.props.toggleExpandedCollapsed(entity); 83 | }} 84 | className={`collapsespace fa ${ 85 | isExpanded ? 'fa-caret-down' : 'fa-caret-right' 86 | }`} 87 | /> 88 | ); 89 | } else { 90 | collapse = ; 91 | } 92 | 93 | // Visibility button. 94 | const visible = 95 | tagName === 'a-scene' 96 | ? entity.object3D.visible 97 | : entity.getAttribute('visible'); 98 | const visibilityButton = ( 99 | 104 | ); 105 | 106 | // Class name. 107 | const className = classNames({ 108 | active: this.props.isSelected, 109 | entity: true, 110 | novisible: !visible, 111 | option: true 112 | }); 113 | 114 | return ( 115 |
116 | 117 | 122 | {visibilityButton} 123 | {printEntity(entity, this.onDoubleClick)} 124 | {collapse} 125 | 126 | 127 | {cloneButton} 128 | {removeButton} 129 | 130 |
131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/components/scenegraph/ToolbarWrapper.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useAuthContext } from '../../contexts'; 3 | import Toolbar from './Toolbar'; 4 | import { isSceneAuthor } from '../../api'; 5 | 6 | function ToolbarWrapper() { 7 | const { currentUser } = useAuthContext(); 8 | const [isAuthor, setIsAuthor] = useState(false); 9 | const currentSceneId = STREET.utils.getCurrentSceneId(); 10 | useEffect(() => { 11 | async function checkAuthorship() { 12 | if (currentUser && currentUser.uid && currentSceneId) { 13 | try { 14 | const isAuthorResult = await isSceneAuthor({ 15 | sceneId: currentSceneId, 16 | authorId: currentUser.uid 17 | }); 18 | setIsAuthor(isAuthorResult); 19 | } catch (error) { 20 | console.error('Error:', error); 21 | } 22 | } 23 | } 24 | 25 | checkAuthorship(); 26 | }, [currentUser, currentSceneId]); 27 | 28 | return ; 29 | } 30 | 31 | export { ToolbarWrapper }; 32 | -------------------------------------------------------------------------------- /src/components/viewport/CameraToolbar/CameraToolbar.component.js: -------------------------------------------------------------------------------- 1 | import './CameraToolbar.scss'; 2 | 3 | import { Component } from 'react'; 4 | 5 | import Events from '../../../lib/Events.js'; 6 | import classNames from 'classnames'; 7 | import { Hint } from '../../components/Tabs/components/index.js'; 8 | 9 | const options = [ 10 | { 11 | value: 'perspective', 12 | event: 'cameraperspectivetoggle', 13 | payload: null, 14 | label: '3D View', 15 | hint: '3D perspective camera with click and drag rotation' 16 | }, 17 | // { value: 'ortholeft', event: 'cameraorthographictoggle', payload: 'left', label: 'Left View' }, 18 | // { value: 'orthoright', event: 'cameraorthographictoggle', payload: 'right', label: 'Right View' }, 19 | { 20 | value: 'orthotop', 21 | event: 'cameraorthographictoggle', 22 | payload: 'top', 23 | label: 'Plan View', 24 | hint: 'Down facing orthographic camera' 25 | }, 26 | // { value: 'orthobottom', event: 'cameraorthographictoggle', payload: 'bottom', label: 'Bottom View' }, 27 | // { value: 'orthoback', event: 'cameraorthographictoggle', payload: 'back', label: 'Back View' }, 28 | { 29 | value: 'orthofront', 30 | event: 'cameraorthographictoggle', 31 | payload: 'front', 32 | label: 'Cross Section', 33 | hint: 'Front facing orthographic camera' 34 | } 35 | ]; 36 | 37 | class CameraToolbar extends Component { 38 | state = { 39 | selectedCamera: 'perspective', 40 | areChangesEmitted: false 41 | }; 42 | 43 | componentDidMount() { 44 | setTimeout(() => { 45 | this.setInitialCamera(); 46 | }, 1); 47 | } 48 | 49 | componentWillUnmount() { 50 | clearTimeout(() => { 51 | this.setInitialCamera(); 52 | }, 1); 53 | } 54 | 55 | setInitialCamera = () => { 56 | if (!this.state.areChangesEmitted) { 57 | const selectedOption = options.find( 58 | ({ value }) => this.state.selectedCamera === value 59 | ); 60 | 61 | this.handleCameraChange(selectedOption); 62 | } 63 | 64 | Events.on('cameratoggle', (data) => 65 | this.setState({ selectedCamera: data.value }) 66 | ); 67 | }; 68 | 69 | handleCameraChange(option) { 70 | this.setState({ selectedCamera: option.value, areChangesEmitted: true }); 71 | Events.emit(option.event, option.payload); 72 | } 73 | 74 | render() { 75 | const className = classNames({ 76 | open: this.state.menuIsOpen 77 | }); 78 | return ( 79 |
80 | {options.map(({ label, value, event, payload, hint }) => ( 81 | 92 | ))} 93 |
94 | ); 95 | } 96 | } 97 | 98 | export { CameraToolbar }; 99 | -------------------------------------------------------------------------------- /src/components/viewport/CameraToolbar/CameraToolbar.scss: -------------------------------------------------------------------------------- 1 | @use '../../../style/variables.scss'; 2 | 3 | #cameraToolbar { 4 | margin-left: 5px; 5 | align-items: center; 6 | display: flex; 7 | column-gap: 12px; 8 | background: rgba(55, 55, 55, 0.4); 9 | border-radius: 16px; 10 | padding: 8px 12px; 11 | transition: 0.2s ease-in-out; 12 | &:hover { 13 | background: rgba(55, 55, 55, 0.6); 14 | } 15 | button { 16 | position: relative; 17 | border: unset; 18 | outline: unset; 19 | background-color: transparent; 20 | padding: 8.5px 12px; 21 | color: variables.$white; 22 | border-radius: 11px; 23 | transition: all 0.3s; 24 | font-size: 16px; 25 | line-height: 19.2px; 26 | font-weight: 400; 27 | &:hover { 28 | cursor: pointer; 29 | background-color: rgba(50, 50, 50, 0.6); 30 | transition: all 0.3s; 31 | } 32 | &:active { 33 | background-color: variables.$black-600; 34 | color: variables.$lightgray-200; 35 | transition: all 0.3s; 36 | } 37 | &:hover .wrapper { 38 | visibility: visible; // need to be global for working with Tab component over wrapper class 39 | opacity: 1; 40 | transition: all 0.3s; 41 | } 42 | } 43 | .selectedCamera { 44 | background-color: variables.$darkgray-300; 45 | transition: all 0.3s; 46 | } 47 | a { 48 | margin-right: 10px; 49 | } 50 | .select__control { 51 | background: none; 52 | font-size: 18px; 53 | padding: 2px 15px 2px 6px; 54 | } 55 | .select__single-value { 56 | color: variables.$white; 57 | &:hover { 58 | color: variables.$blue-100; 59 | } 60 | } 61 | @media screen and (max-width: 1024px) { 62 | #cameraToolbar { 63 | padding: 8px; 64 | margin-left: 64px; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/viewport/CameraToolbar/index.js: -------------------------------------------------------------------------------- 1 | export { CameraToolbar } from './CameraToolbar.component'; 2 | -------------------------------------------------------------------------------- /src/components/viewport/TransformToolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import Events from '../../lib/Events'; 4 | import { sendMetric } from '../../services/ga'; 5 | 6 | var TransformButtons = [ 7 | { value: 'translate', icon: 'fa-arrows-alt' }, 8 | { value: 'rotate', icon: 'fa-repeat' }, 9 | { value: 'scale', icon: 'fa-expand' } 10 | ]; 11 | 12 | export default class TransformToolbar extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | selectedTransform: 'translate', 17 | localSpace: false 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | Events.on('transformmodechange', (mode) => { 23 | this.setState({ selectedTransform: mode }); 24 | }); 25 | 26 | Events.on('transformspacechange', () => { 27 | Events.emit( 28 | 'transformspacechanged', 29 | this.state.localSpace ? 'world' : 'local' 30 | ); 31 | this.setState({ localSpace: !this.state.localSpace }); 32 | }); 33 | } 34 | 35 | changeTransformMode = (mode) => { 36 | this.setState({ selectedTransform: mode }); 37 | Events.emit('transformmodechange', mode); 38 | sendMetric('Toolbar', 'selectHelper', mode); 39 | }; 40 | 41 | onLocalChange = (e) => { 42 | const local = e.target.checked; 43 | this.setState({ localSpace: local }); 44 | Events.emit('transformspacechanged', local ? 'local' : 'world'); 45 | }; 46 | 47 | renderTransformButtons = () => { 48 | return TransformButtons.map( 49 | function (option, i) { 50 | var selected = option.value === this.state.selectedTransform; 51 | var classes = classNames({ 52 | button: true, 53 | fa: true, 54 | [option.icon]: true, 55 | active: selected 56 | }); 57 | 58 | return ( 59 |
65 | ); 66 | }.bind(this) 67 | ); 68 | }; 69 | 70 | render() { 71 | return ( 72 |
73 | {this.renderTransformButtons()} 74 | 75 | 85 | 91 | 92 |
93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/components/viewport/ViewportHUD.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Events from '../../lib/Events'; 3 | import { printEntity } from '../../lib/entity'; 4 | 5 | export default class ViewportHUD extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | hoveredEntity: null, 10 | selectedEntity: null 11 | }; 12 | } 13 | 14 | componentDidMount() { 15 | Events.on('raycastermouseenter', (el) => { 16 | this.setState({ hoveredEntity: el }); 17 | }); 18 | 19 | Events.on('raycastermouseleave', (el) => { 20 | this.setState({ hoveredEntity: el }); 21 | }); 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 |

{printEntity(this.state.hoveredEntity)}

28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/viewport/index.js: -------------------------------------------------------------------------------- 1 | export { CameraToolbar } from './CameraToolbar'; 2 | -------------------------------------------------------------------------------- /src/components/widgets/BooleanWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | 5 | export default class BooleanWidget extends React.Component { 6 | static propTypes = { 7 | componentname: PropTypes.string.isRequired, 8 | entity: PropTypes.object, 9 | name: PropTypes.string.isRequired, 10 | onChange: PropTypes.func, 11 | value: PropTypes.bool 12 | }; 13 | 14 | static defaultProps = { 15 | value: false 16 | }; 17 | 18 | constructor(props) { 19 | super(props); 20 | this.state = { value: this.props.value }; 21 | } 22 | 23 | componentDidUpdate(prevProps) { 24 | if (this.props.value !== prevProps.value) { 25 | this.setState({ value: this.props.value }); 26 | } 27 | } 28 | 29 | onChange = () => { 30 | const value = !this.state.value; 31 | 32 | this.setState({ value }); 33 | if (this.props.onChange) { 34 | this.props.onChange(this.props.name, value); 35 | } 36 | }; 37 | 38 | render() { 39 | const id = this.props.componentname + '.' + this.props.name; 40 | 41 | const checkboxClasses = classNames({ 42 | checkboxAnim: true, 43 | checked: this.state.value 44 | }); 45 | 46 | return ( 47 |
48 | null} 54 | /> 55 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/widgets/ColorWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class ColorWidget extends React.Component { 5 | static propTypes = { 6 | componentname: PropTypes.string.isRequired, 7 | entity: PropTypes.object, 8 | name: PropTypes.string.isRequired, 9 | onChange: PropTypes.func, 10 | value: PropTypes.string 11 | }; 12 | 13 | static defaultProps = { 14 | value: '#ffffff' 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | var value = this.props.value; 21 | this.color = new THREE.Color(); 22 | 23 | this.state = { 24 | value: value, 25 | pickerValue: this.getHexString(value) 26 | }; 27 | } 28 | 29 | setValue(value) { 30 | var pickerValue = this.getHexString(value); 31 | 32 | this.setState({ 33 | value: value, 34 | pickerValue: pickerValue 35 | }); 36 | 37 | if (this.props.onChange) { 38 | this.props.onChange(this.props.name, value); 39 | } 40 | } 41 | 42 | componentDidUpdate(prevProps) { 43 | if (this.props.value !== prevProps.value) { 44 | this.setState({ 45 | value: this.props.value, 46 | pickerValue: this.getHexString(this.props.value) 47 | }); 48 | } 49 | } 50 | 51 | getHexString(value) { 52 | return '#' + this.color.set(value).getHexString(); 53 | } 54 | 55 | onChange = (e) => { 56 | this.setValue(e.target.value); 57 | }; 58 | 59 | onKeyUp = (e) => { 60 | e.stopPropagation(); 61 | // if (e.keyCode === 13) 62 | this.setValue(e.target.value); 63 | }; 64 | 65 | onChangeText = (e) => { 66 | this.setState({ value: e.target.value }); 67 | }; 68 | 69 | render() { 70 | return ( 71 | 72 | 79 | 86 | 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/components/widgets/InputWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class InputWidget extends React.Component { 5 | static propTypes = { 6 | componentname: PropTypes.string, 7 | entity: PropTypes.object, 8 | name: PropTypes.string.isRequired, 9 | onChange: PropTypes.func, 10 | value: PropTypes.any 11 | }; 12 | 13 | constructor(props) { 14 | super(props); 15 | this.state = { value: this.props.value || '' }; 16 | } 17 | 18 | onChange = (e) => { 19 | var value = e.target.value; 20 | this.setState({ value: value }); 21 | if (this.props.onChange) { 22 | this.props.onChange(this.props.name, value); 23 | } 24 | }; 25 | 26 | componentDidUpdate(prevProps) { 27 | if (this.props.value !== prevProps.value) { 28 | this.setState({ value: this.props.value }); 29 | } 30 | } 31 | 32 | render() { 33 | return ( 34 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/widgets/SelectWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Select from 'react-select'; 4 | 5 | export default class SelectWidget extends React.Component { 6 | static propTypes = { 7 | componentname: PropTypes.string.isRequired, 8 | entity: PropTypes.object, 9 | name: PropTypes.string.isRequired, 10 | onChange: PropTypes.func, 11 | options: PropTypes.array.isRequired, 12 | value: PropTypes.string 13 | }; 14 | 15 | constructor(props) { 16 | super(props); 17 | const value = this.props.value || ''; 18 | this.state = { value: { value: value, label: value } }; 19 | } 20 | 21 | onChange = (value) => { 22 | this.setState({ value: value }, () => { 23 | if (this.props.onChange) { 24 | this.props.onChange(this.props.name, value.value); 25 | } 26 | }); 27 | }; 28 | 29 | componentDidUpdate(prevProps) { 30 | const props = this.props; 31 | if (props.value !== prevProps.value) { 32 | this.setState({ 33 | value: { value: props.value, label: props.value } 34 | }); 35 | } 36 | } 37 | 38 | render() { 39 | const options = this.props.options.map((value) => { 40 | return { value: value, label: value }; 41 | }); 42 | 43 | return ( 44 |