├── .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 |
5 |
--------------------------------------------------------------------------------
/assets/ViewerLogo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/card-placeholder.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DStreet/3dstreet-editor/a28193d31377e70314c007a3249f24db89102a4d/assets/favicon.ico
--------------------------------------------------------------------------------
/assets/layers.svg:
--------------------------------------------------------------------------------
1 |
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 |
78 |
91 |
92 | );
93 | }
94 | }
95 |
96 | /* eslint-disable no-unused-vars */
97 | /**
98 | * Check if component has multiplicity.
99 | */
100 | function isComponentInstanced(entity, componentName) {
101 | for (var component in entity.components) {
102 | if (component.substring(0, component.indexOf('__')) === componentName) {
103 | return true;
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/components/components/AddLayerButton/AddLayerButton.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './AddLayerButton.module.scss';
3 | import { Button } from '../Button';
4 | import { Circle20Icon } from '../../../icons';
5 |
6 | const AddLayerButton = ({ onClick }) => (
7 |
8 |
17 |
18 | );
19 |
20 | export { AddLayerButton };
21 |
--------------------------------------------------------------------------------
/src/components/components/AddLayerButton/AddLayerButton.module.scss:
--------------------------------------------------------------------------------
1 | @use '../../../style/variables.scss';
2 |
3 | .wrapper {
4 | .button {
5 | position: absolute;
6 | transform: translateX(-50%);
7 | left: 50%;
8 | border: unset;
9 | bottom: 0px;
10 | width: 64px;
11 | height: 40px;
12 | padding: 4px 20px 4px 20px;
13 | border-radius: 8px 8px 0px 0px;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/components/AddLayerButton/index.js:
--------------------------------------------------------------------------------
1 | export { AddLayerButton } from './AddLayerButton.component.jsx';
2 |
--------------------------------------------------------------------------------
/src/components/components/AddLayerPanel/AddLayerPanel.module.scss:
--------------------------------------------------------------------------------
1 | @use '../../../style/variables.scss';
2 |
3 | .panel {
4 | position: fixed;
5 | left: 0;
6 | bottom: -100%;
7 | width: 100%;
8 | height: 260px;
9 | background: rgba(34, 34, 34, 1);
10 | box-shadow: 2px 0 5px rgba(0, 0, 0, 0.2);
11 | width: calc(100% - 82px);
12 | transition: transform 0.3s ease-in-out;
13 | z-index: 100;
14 | padding: 20px 40px 20px 42px;
15 | margin: 0 auto;
16 | overflow-y: scroll;
17 |
18 | &.open {
19 | bottom: 0;
20 | }
21 |
22 | .dropdown {
23 | min-width: 285px;
24 | height: 36px !important;
25 | padding: 10px;
26 | }
27 |
28 | .button {
29 | display: flex;
30 | align-items: center;
31 | column-gap: 8px;
32 | }
33 |
34 | .buttonLabel {
35 | font-size: 18px !important;
36 | line-height: 21.6px;
37 | color: variables.$white;
38 | }
39 |
40 | .header {
41 | display: flex;
42 | align-items: center;
43 | justify-content: space-between;
44 | margin-bottom: 24px;
45 | }
46 |
47 | .cards {
48 | display: flex;
49 | column-gap: 24px;
50 | flex-wrap: wrap;
51 | row-gap: 30px;
52 |
53 | .card {
54 | .img {
55 | margin-bottom: 15px;
56 | object-fit: cover;
57 | width: 206px;
58 | height: 114px;
59 | }
60 | cursor: pointer;
61 | }
62 |
63 | .body {
64 | display: flex;
65 | align-items: center;
66 | column-gap: 15px;
67 | }
68 |
69 | .description {
70 | color: rgba(182, 182, 182, 1);
71 | }
72 | }
73 |
74 | .closeButton {
75 | position: absolute;
76 | width: 64px;
77 | height: 40px;
78 | transform: translateX(-50%);
79 | top: 0px;
80 | left: 50%;
81 | cursor: pointer;
82 | padding: 4px 20px 4px 20px;
83 | border-radius: 0px 0px 8px 8px;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/components/AddLayerPanel/cardsData.js:
--------------------------------------------------------------------------------
1 | const mixinsData = {
2 | mixinId1: {
3 | img: '',
4 | icon: '',
5 | description: 'Description',
6 | id: 1
7 | },
8 | mixinId2: {
9 | img: '',
10 | icon: '',
11 | description: 'Description',
12 | id: 2
13 | },
14 | mixinId3: {
15 | img: '',
16 | icon: '',
17 | description: 'Description',
18 | id: 3
19 | },
20 | mixinId4: {
21 | img: '',
22 | icon: '',
23 | description: 'Description',
24 | id: 4
25 | }
26 | };
27 |
28 | export default { mixinsData };
29 |
--------------------------------------------------------------------------------
/src/components/components/AddLayerPanel/index.js:
--------------------------------------------------------------------------------
1 | export { AddLayerPanel } from './AddLayerPanel.component.jsx';
2 |
--------------------------------------------------------------------------------
/src/components/components/Button/Button.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { bool, func, node, number, string } from 'prop-types';
3 |
4 | import classNames from 'classnames';
5 | import styles from './Button.module.scss';
6 |
7 | const variants = {
8 | filled: styles.filledButton,
9 | outlined: styles.outlinedButton,
10 | ghost: styles.ghostButton,
11 | toolbtn: styles.toolButton,
12 | white: styles.whiteButton,
13 | custom: styles.customButton
14 | };
15 |
16 | /**
17 | * Button component.
18 | *
19 | * @author Oleksii Medvediev
20 | * @category Components
21 | * @param {{
22 | * className?: string;
23 | * onClick?: () => void;
24 | * type?: string;
25 | * children?: Element;
26 | * variant?: string;
27 | * disabled?: boolean;
28 | * id?: string | number;
29 | * }} props
30 | */
31 | const Button = ({
32 | className,
33 | onClick,
34 | type = 'button',
35 | children,
36 | variant = 'filled',
37 | disabled,
38 | id,
39 | leadingicon,
40 | trailingicon
41 | }) => (
42 |
56 | );
57 |
58 | Button.propTypes = {
59 | className: string,
60 | onClick: func,
61 | type: string,
62 | children: node,
63 | variant: string,
64 | disabled: bool,
65 | id: string || number
66 | };
67 |
68 | export { Button };
69 |
--------------------------------------------------------------------------------
/src/components/components/Button/Button.module.scss:
--------------------------------------------------------------------------------
1 | @use '../../../style/variables.scss';
2 |
3 | .buttonWrapper {
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | column-gap: 8px;
8 | width: fit-content;
9 | padding: 11px 15px !important;
10 | border-radius: 18px;
11 | border: 1px solid transparent;
12 | outline: unset;
13 | background-color: transparent;
14 | font-size: 16px !important;
15 | line-height: 19px;
16 | font-weight: 500;
17 | transition: all 0.3s;
18 |
19 | svg path {
20 | transition: all 0.3s;
21 | }
22 |
23 | &:focus {
24 | outline: none;
25 | }
26 |
27 | &:hover {
28 | cursor: pointer;
29 | }
30 |
31 | &:disabled {
32 | cursor: not-allowed;
33 | }
34 | }
35 |
36 | .filledButton {
37 | background-color: variables.$purple;
38 | color: variables.$white;
39 |
40 | &:hover {
41 | background-color: variables.$purple-100;
42 | transition: all 0.3s;
43 | }
44 |
45 | &:active {
46 | background-color: variables.$purple-200;
47 | color: variables.$lightgray-200;
48 | transition: all 0.3s;
49 | }
50 |
51 | &:disabled {
52 | background-color: rgba(237, 235, 239, 0.5);
53 | color: rgba(50, 50, 50, 0.4);
54 | transition: all 0.3s;
55 | }
56 |
57 | &:focus {
58 | border: 1px solid variables.$white;
59 | }
60 |
61 | &.icon {
62 | display: flex;
63 | justify-content: center;
64 | align-items: center;
65 | width: 24px;
66 | height: 24px;
67 | }
68 | }
69 |
70 | .outlinedButton {
71 | background-color: transparent;
72 | color: variables.$purple;
73 | border-color: variables.$purple;
74 |
75 | &:hover {
76 | color: variables.$purple-100;
77 | border-color: variables.$purple-100;
78 | transition: all 0.3s;
79 | }
80 |
81 | &:active {
82 | color: variables.$purple-200;
83 | border-color: variables.$purple-200;
84 | transition: all 0.3s;
85 | }
86 |
87 | &:focus {
88 | border: 1px solid variables.$white;
89 | }
90 |
91 | &:disabled {
92 | color: rgba(237, 235, 239, 0.5);
93 | border-color: rgba(237, 235, 239, 0.5);
94 | transition: all 0.3s;
95 | }
96 | }
97 |
98 | .ghostButton {
99 | color: variables.$purple;
100 |
101 | &:hover {
102 | color: variables.$purple-100;
103 | transition: all 0.3s;
104 | }
105 |
106 | &:active {
107 | color: variables.$purple-200;
108 | transition: all 0.3s;
109 | }
110 |
111 | &:disabled {
112 | color: rgba(237, 235, 239, 0.5);
113 | transition: all 0.3s;
114 | }
115 | }
116 |
117 | .toolButton {
118 | background-color: rgba(50, 50, 50, 0.8);
119 | color: variables.$white;
120 |
121 | &:hover {
122 | background-color: variables.$black-400;
123 | transition: all 0.3s;
124 | }
125 |
126 | &:active {
127 | background-color: variables.$black-700;
128 | color: variables.$lightgray-200;
129 | transition: all 0.3s;
130 | }
131 |
132 | &:disabled {
133 | background-color: rgba(50, 50, 50, 0.6);
134 | color: variables.$gray-500;
135 | transition: all 0.3s;
136 | }
137 | }
138 |
139 | .whiteButton {
140 | background-color: variables.$lightgray-500;
141 | color: variables.$gray-200;
142 | border-radius: 18px;
143 | box-shadow: 0px 7px 3px 0px rgba(0, 0, 0, 0.32);
144 |
145 | &:active {
146 | border: 1px solid variables.$white;
147 | background-color: variables.$lightgray-400;
148 | }
149 |
150 | &:focus {
151 | border: 1px solid variables.$white;
152 | }
153 |
154 | &:hover {
155 | background-color: variables.$white;
156 | transition: all 0.3s;
157 | }
158 |
159 | &:disabled {
160 | background-color: variables.$gray-500;
161 | color: rgba(50, 50, 50, 0.4);
162 | transition: all 0.3s;
163 | }
164 | }
165 |
166 | .customButton {
167 | background-color: rgba(50, 50, 50, 0.8);
168 | color: #dbdbdb;
169 | border-radius: 10px;
170 |
171 | &:active {
172 | background-color: #282828;
173 | }
174 |
175 | &:focus {
176 | border: 1px solid #fff;
177 | background-color: #2d2d2d;
178 | }
179 |
180 | &:hover {
181 | background-color: #2d2d2d;
182 | transition: all 0.3s;
183 | }
184 |
185 | &:disabled {
186 | color: #474747;
187 | transition: all 0.3s;
188 | }
189 | }
190 |
191 | .icon {
192 | display: flex;
193 | justify-content: center;
194 | align-items: center;
195 | width: 24px;
196 | height: 24px;
197 | }
198 |
--------------------------------------------------------------------------------
/src/components/components/Button/index.js:
--------------------------------------------------------------------------------
1 | export { Button } from './Button.component.jsx';
2 |
--------------------------------------------------------------------------------
/src/components/components/Checkbox/Checkbox.component.jsx:
--------------------------------------------------------------------------------
1 | import { bool, func, string } from 'prop-types';
2 |
3 | import classNames from 'classnames';
4 | import styles from './Checkbox.module.scss';
5 | import { v4 } from 'uuid';
6 |
7 | const checkboxId = v4();
8 |
9 | /**
10 | * Checkbox component.
11 | *
12 | * @author Oleksii Medvediev
13 | * @category Components
14 | * @param {{
15 | * isChecked: boolean;
16 | * onChange: (value: boolean) => void;
17 | * label?: string;
18 | * className?: string;
19 | * id?: string;
20 | * disabled?: boolean
21 | * }} props
22 | */
23 | const Checkbox = ({
24 | isChecked,
25 | onChange,
26 | label,
27 | className,
28 | id = checkboxId,
29 | disabled
30 | }) => {
31 | return (
32 |
33 |
34 |
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 |
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 |
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 |
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 |
23 |
29 |
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 |
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 |
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 |
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 |
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 |
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/widgets/Vec2Widget.js:
--------------------------------------------------------------------------------
1 | import NumberWidget from './NumberWidget';
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 | import { areVectorsEqual } from '../../lib/utils.js';
5 |
6 | export default class Vec2Widget extends React.Component {
7 | static propTypes = {
8 | componentname: PropTypes.string,
9 | entity: PropTypes.object,
10 | onChange: PropTypes.func,
11 | value: PropTypes.object.isRequired
12 | };
13 |
14 | constructor(props) {
15 | super(props);
16 | this.state = {
17 | x: props.value.x,
18 | y: props.value.y
19 | };
20 | }
21 |
22 | onChange = (name, value) => {
23 | this.setState({ [name]: parseFloat(value.toFixed(5)) }, () => {
24 | if (this.props.onChange) {
25 | this.props.onChange(name, this.state);
26 | }
27 | });
28 | };
29 |
30 | componentDidUpdate() {
31 | const props = this.props;
32 | if (!areVectorsEqual(props.value, this.state)) {
33 | this.setState({
34 | x: props.value.x,
35 | y: props.value.y
36 | });
37 | }
38 | }
39 |
40 | render() {
41 | const widgetProps = {
42 | componentname: this.props.componentname,
43 | entity: this.props.entity,
44 | onChange: this.onChange
45 | };
46 |
47 | return (
48 |
49 |
50 |
51 |
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/widgets/Vec3Widget.js:
--------------------------------------------------------------------------------
1 | import NumberWidget from './NumberWidget';
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 | import { areVectorsEqual } from '../../lib/utils.js';
5 | export default class Vec3Widget extends React.Component {
6 | static propTypes = {
7 | componentname: PropTypes.string,
8 | entity: PropTypes.object,
9 | onChange: PropTypes.func,
10 | value: PropTypes.object.isRequired
11 | };
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | x: props.value.x,
17 | y: props.value.y,
18 | z: props.value.z
19 | };
20 | }
21 |
22 | onChange = (name, value) => {
23 | this.setState({ [name]: parseFloat(value.toFixed(5)) }, () => {
24 | if (this.props.onChange) {
25 | this.props.onChange(name, this.state);
26 | }
27 | });
28 | };
29 |
30 | componentDidUpdate() {
31 | const props = this.props;
32 | if (!areVectorsEqual(props.value, this.state)) {
33 | this.setState({
34 | x: props.value.x,
35 | y: props.value.y,
36 | z: props.value.z
37 | });
38 | }
39 | }
40 |
41 | render() {
42 | const widgetProps = {
43 | componentname: this.props.componentname,
44 | entity: this.props.entity,
45 | onChange: this.onChange
46 | };
47 |
48 | return (
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/widgets/Vec4Widget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { areVectorsEqual } from '../../lib/utils.js';
4 |
5 | import NumberWidget from './NumberWidget';
6 |
7 | export default class Vec4Widget extends React.Component {
8 | static propTypes = {
9 | componentname: PropTypes.string,
10 | entity: PropTypes.object,
11 | onChange: PropTypes.func,
12 | value: PropTypes.object.isRequired
13 | };
14 |
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | x: props.value.x,
19 | y: props.value.y,
20 | z: props.value.z,
21 | w: props.value.w
22 | };
23 | }
24 |
25 | onChange = (name, value) => {
26 | this.setState({ [name]: parseFloat(value.toFixed(5)) }, () => {
27 | if (this.props.onChange) {
28 | this.props.onChange(name, this.state);
29 | }
30 | });
31 | };
32 |
33 | componentDidUpdate() {
34 | const props = this.props;
35 | if (!areVectorsEqual(props.value, this.state)) {
36 | this.setState({
37 | x: props.value.x,
38 | y: props.value.y,
39 | z: props.value.z,
40 | w: props.value.w
41 | });
42 | }
43 | }
44 |
45 | render() {
46 | const widgetProps = {
47 | componentname: this.props.componentname,
48 | entity: this.props.entity,
49 | onChange: this.onChange
50 | };
51 |
52 | return (
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/widgets/index.js:
--------------------------------------------------------------------------------
1 | export { default as BooleanWidget } from './BooleanWidget';
2 | export { default as ColorWidget } from './ColorWidget';
3 | export { default as InputWidget } from './InputWidget';
4 | export { default as NumberWidget } from './NumberWidget';
5 | export { default as SelectWidget } from './SelectWidget';
6 | export { default as TextureWidget } from './TextureWidget';
7 | export { default as Vec4Widget } from './Vec4Widget';
8 | export { default as Vec3Widget } from './Vec3Widget';
9 | export { default as Vec2Widget } from './Vec2Widget';
10 |
--------------------------------------------------------------------------------
/src/contexts/Auth.context.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from 'react';
2 | import { auth } from '../services/firebase';
3 | import PropTypes from 'prop-types';
4 | import { isUserPro, isUserBeta } from '../api/user';
5 |
6 | const AuthContext = createContext({
7 | currentUser: null,
8 | setCurrentUser: (user) => {}
9 | });
10 |
11 | const AuthProvider = ({ children }) => {
12 | const [currentUser, setCurrentUser] = useState(null);
13 |
14 | useEffect(() => {
15 | const fetchUserData = async (user) => {
16 | if (!user) {
17 | localStorage.removeItem('token');
18 | setCurrentUser(null);
19 | return;
20 | }
21 |
22 | localStorage.setItem('token', await user.getIdToken());
23 |
24 | const isPro = await isUserPro(user);
25 | const isBeta = await isUserBeta(user);
26 | const enrichedUser = { ...user, isPro, isBeta };
27 |
28 | setCurrentUser(enrichedUser);
29 |
30 | };
31 |
32 | const unsubscribe = auth.onAuthStateChanged((user) => {
33 | fetchUserData(user);
34 | });
35 |
36 | return () => unsubscribe();
37 | }, []);
38 |
39 | return (
40 |
41 | {children}
42 |
43 | );
44 | };
45 |
46 | AuthProvider.propTypes = {
47 | children: PropTypes.node.isRequired
48 | };
49 |
50 | const useAuthContext = () => useContext(AuthContext);
51 |
52 | export { AuthProvider, useAuthContext };
53 |
--------------------------------------------------------------------------------
/src/contexts/index.js:
--------------------------------------------------------------------------------
1 | export { AuthProvider, useAuthContext } from './Auth.context';
2 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { useClickOutside } from './use-click-outside';
2 |
--------------------------------------------------------------------------------
/src/hooks/use-click-outside.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from 'react';
2 |
3 | /**
4 | * useClickOutside hook.
5 | * Used fo invoking callbacks, when clicking outside the ref element.
6 | *
7 | * @author Oleksii Medvediev
8 | * @category Hooks
9 | * @param { RefObject } ref - ref element to watch on.
10 | * @param { Function } handleClickOutside - a callback function invoked when clicked outside the ref element.
11 | */
12 | const useClickOutside = (ref, handleClickOutside) => {
13 | const handleClick = useCallback(
14 | (event) => {
15 | if (
16 | ref.current &&
17 | !ref.current.contains(event.target) &&
18 | event.button !== 2
19 | ) {
20 | handleClickOutside();
21 | }
22 | },
23 | [handleClickOutside, ref]
24 | );
25 |
26 | useEffect(() => {
27 | document.addEventListener('pointerdown', handleClick);
28 | return () => document.removeEventListener('pointerdown', handleClick);
29 | }, [handleClick]);
30 | };
31 |
32 | export { useClickOutside };
33 |
--------------------------------------------------------------------------------
/src/icons/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | Camera32Icon,
3 | Save24Icon,
4 | Load24Icon,
5 | Upload24Icon,
6 | Cross32Icon,
7 | Cross24Icon,
8 | Compass32Icon,
9 | ArrowDown24Icon,
10 | ArrowUp24Icon,
11 | CheckIcon,
12 | DropdownArrowIcon,
13 | Cloud24Icon,
14 | Mangnifier20Icon,
15 | Edit32Icon,
16 | CheckMark32Icon,
17 | Copy32Icon,
18 | DropdownIcon,
19 | Loader,
20 | RemixIcon,
21 | ArrowLeftIcon,
22 | ArrowRightIcon,
23 | LayersIcon,
24 | GoogleSignInButtonSVG,
25 | Chevron24Down,
26 | Circle20Icon,
27 | Plus20Circle
28 | } from './icons.jsx';
29 |
--------------------------------------------------------------------------------
/src/lib/Events.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'events';
2 |
3 | const Events = new EventEmitter();
4 | Events.setMaxListeners(0);
5 |
6 | export default Events;
7 |
--------------------------------------------------------------------------------
/src/lib/assetsLoader.js:
--------------------------------------------------------------------------------
1 | import Events from './Events';
2 |
3 | const assetsBaseUrl = 'https://aframe.io/sample-assets/';
4 | const assetsRelativeUrl = { images: 'dist/images.json' };
5 |
6 | /**
7 | * Asynchronously load and register components from the registry.
8 | */
9 | export function AssetsLoader() {
10 | this.images = [];
11 | this.hasLoaded = false;
12 | }
13 |
14 | AssetsLoader.prototype = {
15 | /**
16 | * XHR the assets JSON.
17 | */
18 | load: function () {
19 | var xhr = new XMLHttpRequest();
20 | var url = assetsBaseUrl + assetsRelativeUrl.images;
21 |
22 | // @todo Remove the sync call and use a callback
23 | xhr.open('GET', url);
24 |
25 | xhr.onload = () => {
26 | var data = JSON.parse(xhr.responseText);
27 | this.images = data.images;
28 | this.images.forEach((image) => {
29 | image.fullPath = assetsBaseUrl + data.basepath.images + image.path;
30 | image.fullThumbPath =
31 | assetsBaseUrl + data.basepath.images_thumbnails + image.thumbnail;
32 | });
33 | Events.emit('assetsimagesload', this.images);
34 | };
35 | xhr.onerror = () => {
36 | console.error('Error loading registry file.');
37 | };
38 | xhr.send();
39 |
40 | this.hasLoaded = true;
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/src/lib/assetsUtils.js:
--------------------------------------------------------------------------------
1 | export function insertNewAsset(
2 | type,
3 | id,
4 | src,
5 | anonymousCrossOrigin,
6 | onLoadedCallback
7 | ) {
8 | var element = null;
9 | switch (type) {
10 | case 'img':
11 | {
12 | element = document.createElement('img');
13 | element.id = id;
14 | element.src = src;
15 | if (anonymousCrossOrigin) {
16 | element.crossOrigin = 'anonymous';
17 | }
18 | }
19 | break;
20 | }
21 |
22 | if (element) {
23 | element.onload = function () {
24 | if (onLoadedCallback) {
25 | onLoadedCallback();
26 | }
27 | };
28 | document.getElementsByTagName('a-assets')[0].appendChild(element);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/lib/cameras.js:
--------------------------------------------------------------------------------
1 | import Events from './Events';
2 |
3 | // Save ortho camera FOV / position before switching to restore later.
4 | let currentOrthoDir = '';
5 | const orthoCameraMemory = {
6 | left: { position: new THREE.Vector3(-10, 0, 0), rotation: new THREE.Euler() },
7 | right: { position: new THREE.Vector3(10, 0, 0), rotation: new THREE.Euler() },
8 | top: { position: new THREE.Vector3(0, 10, 0), rotation: new THREE.Euler() },
9 | bottom: {
10 | position: new THREE.Vector3(0, -10, 0),
11 | rotation: new THREE.Euler()
12 | },
13 | back: { position: new THREE.Vector3(0, 0, -10), rotation: new THREE.Euler() },
14 | front: { position: new THREE.Vector3(0, 0, 10), rotation: new THREE.Euler() }
15 | };
16 |
17 | /**
18 | * Initialize various cameras, store original one.
19 | */
20 | export function initCameras(inspector) {
21 | const sceneEl = inspector.sceneEl;
22 |
23 | const originalCamera = (inspector.currentCameraEl = sceneEl.camera.el);
24 | inspector.currentCameraEl.setAttribute(
25 | 'data-aframe-inspector-original-camera',
26 | ''
27 | );
28 |
29 | // If the current camera is the default, we should prevent AFRAME from
30 | // remove it once when we inject the editor's camera.
31 | if (inspector.currentCameraEl.hasAttribute('data-aframe-default-camera')) {
32 | inspector.currentCameraEl.removeAttribute('data-aframe-default-camera');
33 | inspector.currentCameraEl.setAttribute(
34 | 'data-aframe-inspector',
35 | 'default-camera'
36 | );
37 | }
38 |
39 | inspector.currentCameraEl.setAttribute('camera', 'active', false);
40 |
41 | // Create Inspector camera.
42 | const perspectiveCamera = (inspector.camera = new THREE.PerspectiveCamera());
43 | perspectiveCamera.far = 10000;
44 | perspectiveCamera.near = 0.01;
45 | perspectiveCamera.position.set(0, 15, 30);
46 | perspectiveCamera.lookAt(new THREE.Vector3(0, 1.6, -1));
47 | perspectiveCamera.updateMatrixWorld();
48 | sceneEl.object3D.add(perspectiveCamera);
49 | sceneEl.camera = perspectiveCamera;
50 |
51 | const ratio = sceneEl.canvas.width / sceneEl.canvas.height;
52 | const orthoCamera = new THREE.OrthographicCamera(
53 | -40 * ratio,
54 | 40 * ratio,
55 | 40,
56 | -40
57 | );
58 | sceneEl.object3D.add(orthoCamera);
59 |
60 | const cameras = (inspector.cameras = {
61 | perspective: perspectiveCamera,
62 | original: originalCamera,
63 | ortho: orthoCamera
64 | });
65 |
66 | // Command to switch to perspective.
67 | Events.on('cameraperspectivetoggle', () => {
68 | saveOrthoCamera(inspector.camera, currentOrthoDir);
69 | sceneEl.camera = inspector.camera = cameras.perspective;
70 | Events.emit('cameratoggle', {
71 | camera: inspector.camera,
72 | value: 'perspective'
73 | });
74 | });
75 |
76 | // Command to switch to ortographic.
77 | Events.on('cameraorthographictoggle', (dir) => {
78 | saveOrthoCamera(inspector.camera, currentOrthoDir);
79 | sceneEl.camera = inspector.camera = cameras.ortho;
80 | currentOrthoDir = dir;
81 | setOrthoCamera(cameras.ortho, dir, ratio);
82 |
83 | // Set initial rotation for the respective orthographic camera.
84 | if (
85 | cameras.ortho.rotation.x === 0 &&
86 | cameras.ortho.rotation.y === 0 &&
87 | cameras.ortho.rotation.z === 0
88 | ) {
89 | cameras.ortho.lookAt(0, 0, 0);
90 | }
91 | Events.emit('cameratoggle', {
92 | camera: inspector.camera,
93 | value: `ortho${dir}`
94 | });
95 | });
96 |
97 | return inspector.cameras;
98 | }
99 |
100 | function saveOrthoCamera(camera, dir) {
101 | if (camera.type !== 'OrthographicCamera') {
102 | return;
103 | }
104 | const info = orthoCameraMemory[dir];
105 | info.position.copy(camera.position);
106 | info.rotation.copy(camera.rotation);
107 | info.left = camera.left;
108 | info.right = camera.right;
109 | info.top = camera.top;
110 | info.bottom = camera.bottom;
111 | }
112 |
113 | function setOrthoCamera(camera, dir, ratio) {
114 | const info = orthoCameraMemory[dir];
115 | camera.left = info.left || -40 * ratio;
116 | camera.right = info.right || 40 * ratio;
117 | camera.top = info.top || 40;
118 | camera.bottom = info.bottom || -40;
119 | camera.position.copy(info.position);
120 | camera.rotation.copy(info.rotation);
121 | }
122 |
--------------------------------------------------------------------------------
/src/lib/history.js:
--------------------------------------------------------------------------------
1 | import Events from './Events';
2 |
3 | export const updates = {};
4 |
5 | /**
6 | * Store change to export.
7 | *
8 | * payload: entity, component, property, value.
9 | */
10 | Events.on('entityupdate', (payload) => {
11 | let value = payload.value;
12 |
13 | const entity = payload.entity;
14 | updates[entity.id] = updates[entity.id] || {};
15 |
16 | const component = AFRAME.components[payload.component];
17 | if (component) {
18 | if (payload.property) {
19 | updates[entity.id][payload.component] =
20 | updates[entity.id][payload.component] || {};
21 | if (component.schema[payload.property]) {
22 | value = component.schema[payload.property].stringify(payload.value);
23 | }
24 | updates[entity.id][payload.component][payload.property] = value;
25 | } else {
26 | value = component.schema.stringify(payload.value);
27 | updates[entity.id][payload.component] = value;
28 | }
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/src/lib/toolbar.js:
--------------------------------------------------------------------------------
1 | import Events from './Events';
2 |
3 | export function inputStreetmix() {
4 | const streetmixURL = prompt(
5 | 'Please enter a Streetmix URL',
6 | 'https://streetmix.net/kfarr/3/example-street'
7 | );
8 |
9 | setTimeout(function () {
10 | window.location.hash = streetmixURL;
11 | });
12 |
13 | const streetContainerEl = document.getElementById('street-container');
14 |
15 | while (streetContainerEl.firstChild) {
16 | streetContainerEl.removeChild(streetContainerEl.lastChild);
17 | }
18 |
19 | streetContainerEl.innerHTML =
20 | '';
23 |
24 | // update sceneGraph
25 | Events.emit('entitycreated', streetContainerEl.sceneEl);
26 | }
27 |
28 | export function createElementsForScenesFromJSON(streetData) {
29 | const streetContainerEl = document.getElementById('street-container');
30 |
31 | while (streetContainerEl.firstChild) {
32 | streetContainerEl.removeChild(streetContainerEl.lastChild);
33 | }
34 |
35 | if (!Array.isArray(streetData)) {
36 | console.error('Invalid data format. Expected an array.');
37 | return;
38 | }
39 |
40 | STREET.utils.createEntities(streetData, streetContainerEl);
41 | }
42 |
43 | export function fileJSON(event) {
44 | let reader = new FileReader();
45 |
46 | reader.onload = function () {
47 | STREET.utils.createElementsFromJSON(reader.result);
48 | // update sceneGraph
49 | Events.emit('entitycreated', streetContainerEl.sceneEl);
50 | };
51 |
52 | reader.readAsText(event.target.files[0]);
53 | }
54 |
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | export function getNumber(value) {
2 | return parseFloat(value.toFixed(3));
3 | }
4 |
5 | export function getMajorVersion(version) {
6 | var major = version.split('.');
7 | var clean = false;
8 | for (var i = 0; i < major.length; i++) {
9 | if (clean) {
10 | major[i] = 0;
11 | } else if (major[i] !== '0') {
12 | clean = true;
13 | }
14 | }
15 | return major.join('.');
16 | }
17 |
18 | export function equal(var1, var2) {
19 | var keys1;
20 | var keys2;
21 | var type1 = typeof var1;
22 | var type2 = typeof var2;
23 | if (type1 !== type2) {
24 | return false;
25 | }
26 | if (type1 !== 'object' || var1 === null || var2 === null) {
27 | return var1 === var2;
28 | }
29 | keys1 = Object.keys(var1);
30 | keys2 = Object.keys(var2);
31 | if (keys1.length !== keys2.length) {
32 | return false;
33 | }
34 | for (var i = 0; i < keys1.length; i++) {
35 | if (!equal(var1[keys1[i]], var2[keys2[i]])) {
36 | return false;
37 | }
38 | }
39 | return true;
40 | }
41 |
42 | export function getOS() {
43 | var userAgent = window.navigator.userAgent;
44 | var platform = window.navigator.platform;
45 | var macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'];
46 | var windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
47 | var iosPlatforms = ['iPhone', 'iPad', 'iPod'];
48 | var os = null;
49 |
50 | if (macosPlatforms.indexOf(platform) !== -1) {
51 | os = 'macos';
52 | } else if (iosPlatforms.indexOf(platform) !== -1) {
53 | os = 'ios';
54 | } else if (windowsPlatforms.indexOf(platform) !== -1) {
55 | os = 'windows';
56 | } else if (/Android/.test(userAgent)) {
57 | os = 'android';
58 | } else if (!os && /Linux/.test(platform)) {
59 | os = 'linux';
60 | }
61 |
62 | return os;
63 | }
64 |
65 | export function injectCSS(url) {
66 | var link = document.createElement('link');
67 | link.href = url;
68 | link.type = 'text/css';
69 | link.rel = 'stylesheet';
70 | link.media = 'screen,print';
71 | link.setAttribute('data-aframe-inspector', 'style');
72 | document.head.appendChild(link);
73 | }
74 |
75 | export function injectJS(url, onLoad, onError) {
76 | var link = document.createElement('script');
77 | link.src = url;
78 | link.charset = 'utf-8';
79 | link.setAttribute('data-aframe-inspector', 'style');
80 |
81 | if (onLoad) {
82 | link.addEventListener('load', onLoad);
83 | }
84 |
85 | if (onError) {
86 | link.addEventListener('error', onError);
87 | }
88 |
89 | document.head.appendChild(link);
90 | }
91 |
92 | export function saveString(text, filename, mimeType) {
93 | saveBlob(new Blob([text], { type: mimeType }), filename);
94 | }
95 |
96 | export function saveBlob(blob, filename) {
97 | var link = document.createElement('a');
98 | link.style.display = 'none';
99 | document.body.appendChild(link);
100 | link.href = URL.createObjectURL(blob);
101 | link.download = filename || 'ascene.html';
102 | link.click();
103 | // URL.revokeObjectURL(url); breaks Firefox...
104 | }
105 |
106 | export function areVectorsEqual(v1, v2) {
107 | return (
108 | Object.is(v1.x, v2.x) &&
109 | Object.is(v1.y, v2.y) &&
110 | Object.is(v1.z, v2.z) &&
111 | Object.is(v1.w, v2.w)
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/src/services/firebase.js:
--------------------------------------------------------------------------------
1 | import { initializeApp } from 'firebase/app';
2 | import { getAuth } from 'firebase/auth';
3 | import { getStorage } from 'firebase/storage';
4 | import { getFirestore } from 'firebase/firestore';
5 |
6 | const firebaseConfig = {
7 | apiKey: process.env.FIREBASE_API_KEY,
8 | authDomain: process.env.FIREBASE_AUTH_DOMAIN,
9 | projectId: process.env.FIREBASE_PROJECT_ID,
10 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
11 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
12 | appId: process.env.FIREBASE_APP_ID,
13 | measurementId: process.env.FIREBASE_MEASUREMENT_ID
14 | };
15 |
16 | const app = initializeApp(firebaseConfig);
17 | const auth = getAuth(app);
18 | const storage = getStorage(app);
19 | const db = getFirestore(app);
20 |
21 | export { auth, storage, db };
22 |
--------------------------------------------------------------------------------
/src/services/ga.js:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga4';
2 |
3 | const sendMetric = (category, action, label) => {
4 | ReactGA.event({
5 | category,
6 | action,
7 | label: label
8 | });
9 | };
10 |
11 | export { sendMetric };
12 |
--------------------------------------------------------------------------------
/src/style/entity.scss:
--------------------------------------------------------------------------------
1 | @use './variables.scss';
2 |
3 | .entityPrint {
4 | overflow: hidden;
5 | text-overflow: ellipsis;
6 | line-height: 16.8px;
7 | }
8 |
9 | .entityName {
10 | width: 230px;
11 | position: relative;
12 | white-space: nowrap;
13 | font-style: normal;
14 | font-weight: 500;
15 | font-size: 14px;
16 | line-height: 14px;
17 | color: variables.$white;
18 | }
19 |
20 | [data-entity-name-type='class'] {
21 | color: variables.$white;
22 | }
23 |
24 | [data-entity-name-type='mixin'] {
25 | color: variables.$orange-100;
26 | }
27 |
--------------------------------------------------------------------------------
/src/style/viewport.scss:
--------------------------------------------------------------------------------
1 | @use './variables.scss';
2 |
3 | #viewportBar {
4 | align-items: center;
5 | color: variables.$lightgray-100;
6 | position: fixed;
7 | display: flex;
8 | flex-grow: 2;
9 | height: 48px;
10 | font-size: 20px;
11 | justify-content: center;
12 | left: 0;
13 | margin: 0 auto;
14 | right: 0;
15 | top: 36px;
16 | }
17 | .toolbarButtons {
18 | display: none;
19 | position: relative;
20 | * {
21 | margin-left: 0;
22 | padding: 8px;
23 | vertical-align: middle;
24 | }
25 | a.button {
26 | margin: 0 6px 0 0;
27 | &:not(.active):hover {
28 | background-color: variables.$darkgray-600;
29 | }
30 | }
31 | .active {
32 | background-color: variables.$blue-100;
33 | color: variables.$white;
34 | &:hover {
35 | color: variables.$white !important;
36 | }
37 | }
38 | }
39 | .local-transform {
40 | padding-left: 10px;
41 | label {
42 | color: variables.$lightgray;
43 | padding-left: 5px;
44 | }
45 | a.button {
46 | padding-top: 0;
47 | }
48 | }
49 | #cameraSelect {
50 | cursor: pointer;
51 | width: 200px;
52 | .select__dropdown-indicator {
53 | padding-left: 3px;
54 | padding-right: 3px;
55 | }
56 | }
57 | #cameraToolbar {
58 | &:has(> .select__menu) {
59 | background: variables.$darkgray-300 !important;
60 | }
61 | &.open .select__dropdown-indicator {
62 | background-image: url(variables.$selectDropdownIndicatorActive);
63 | }
64 | }
65 | #viewportHud {
66 | display: none;
67 |
68 | @media (min-width: 1024px) {
69 | display: block;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/style/widgets.scss:
--------------------------------------------------------------------------------
1 | @use './variables.scss';
2 | .Select-control {
3 | background-color: variables.$black !important;
4 | border: none;
5 | border-radius: 0;
6 | color: variables.$blue-100;
7 | }
8 | .Select-menu-outer {
9 | border: none;
10 | }
11 | .Select-menu-outer .is-focused {
12 | background-color: variables.$blue-100 !important;
13 | color: variables.$lightgray-100;
14 | }
15 | .Select-option {
16 | background-color: variables.$black !important;
17 | }
18 | .select-widget {
19 | display: inline-block;
20 | width: 157px;
21 | }
22 | .Select-placeholder,
23 | .Select--single > .Select-control .Select-value {
24 | color: variables.$blue-100 !important;
25 | }
26 | .Select-value-label {
27 | color: variables.$blue-100 !important;
28 | }
29 |
30 | /* Dropdown menu */
31 | .dropbtn {
32 | border: none;
33 | color: variables.$lightgray-100;
34 | cursor: pointer;
35 | }
36 | .dropdown {
37 | display: inline-block;
38 | position: relative;
39 | }
40 | .dropdown-content {
41 | background-color: variables.$white-200;
42 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
43 | display: none;
44 | left: 8px;
45 | min-width: 38px;
46 | position: absolute;
47 | z-index: 999;
48 | }
49 | .dropdown-content a {
50 | background-color: variables.$black-500;
51 | color: variables.$gray-600;
52 | display: block;
53 | padding: 10px 14px;
54 | text-decoration: none;
55 | }
56 | .dropdown-content a:hover {
57 | background-color: variables.$aqua-100;
58 | color: variables.$lightgray-100;
59 | }
60 | .dropdownhover .dropdown-content {
61 | display: block;
62 | }
63 | .dropdownhover .dropbtn {
64 | color: variables.$blue-100;
65 | }
66 |
--------------------------------------------------------------------------------
/src/viewer-styles.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;500');
2 | html {
3 | font-family: 'Lato', sans-serif;
4 | }
5 |
6 | * {
7 | padding: 0;
8 | margin: 0;
9 | }
10 |
11 | body {
12 | background-color: #c4c4c4;
13 | color: #283237;
14 | }
15 |
16 | /********* viewer header css *********/
17 |
18 | .viewer-header-wrapper {
19 | background-color: transparent;
20 | width: 100%;
21 | padding: 0px 0;
22 | position: fixed;
23 | top: 39px;
24 | left: 40px;
25 | z-index: 9;
26 | }
27 |
28 | .viewer-header-wrapper {
29 | width: 100%;
30 | display: inline-flex;
31 | align-items: center;
32 | }
33 |
34 | .viewer-logo-img {
35 | height: 43px;
36 | width: 370px;
37 | }
38 |
39 | .viewer-logo-start-editor-button {
40 | border: none;
41 | background: none;
42 | cursor: pointer;
43 | }
44 |
45 | .viewer-logo-start-editor-button:hover,
46 | .viewer-logo-start-editor-button.hover {
47 | transition: all 0.25s ease-in-out 0s;
48 | -webkit-filter: brightness(0.75);
49 | -moz-filter: brightness(0.75);
50 | -ms-filter: brightness(0.75);
51 | filter: brightness(0.75);
52 | }
53 |
54 | /********* right menu css *********/
55 |
56 | .right-fixed {
57 | position: fixed;
58 | right: 0;
59 | margin-top: 40vh;
60 | z-index: 1;
61 | }
62 |
63 | .right-menu {
64 | padding-left: 0;
65 | text-align: right;
66 | }
67 |
68 | .right-menu li {
69 | list-style: none;
70 | margin-right: 0;
71 | position: relative;
72 | }
73 |
74 | .right-menu li a {
75 | background: rgba(50, 50, 50, 0.5);
76 | padding: 0;
77 | list-style: none;
78 | margin: 10px 0;
79 | border-radius: 12px 0 0 12px;
80 | display: inline-flex;
81 | font-size: 18px;
82 | align-items: center;
83 | transition: all 0.4s ease-in-out 0s;
84 | }
85 |
86 | .right-menu li a:hover {
87 | background: #6100ff;
88 | }
89 |
90 | .right-menu li a span {
91 | margin-left: 16px;
92 | transition: all 0.4s ease-in-out 0s;
93 | text-align: left;
94 | width: 200px;
95 | letter-spacing: 0.5px;
96 | font-size: 18px;
97 | margin-right: -270px;
98 | padding-left: 60px;
99 | color: rgba(50, 50, 50, 0.5);
100 | }
101 |
102 | .right-menu li a:hover span {
103 | margin-right: 0;
104 | padding-left: 0;
105 | color: #fff;
106 | }
107 |
108 | .right-menu li a {
109 | color: #fff;
110 | text-decoration: none;
111 | }
112 |
113 | .right-menu li a img {
114 | vertical-align: middle;
115 | width: 32px;
116 | height: 32px;
117 | z-index: 999;
118 | padding: 16px;
119 | }
120 |
121 | .right-menu li a:hover img {
122 | background: #6100ff;
123 | z-index: 9;
124 | }
125 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const Dotenv = require('dotenv-webpack');
4 |
5 | module.exports = {
6 | mode: 'development',
7 | devServer: {
8 | hot: true,
9 | liveReload: false,
10 | port: 3333,
11 | static: {
12 | directory: '.'
13 | }
14 | },
15 | devtool: 'source-map',
16 | entry: './src/index.js',
17 | output: {
18 | path: path.join(__dirname, 'dist'),
19 | filename: '3dstreet-editor.js',
20 | publicPath: '/dist/'
21 | },
22 | externals: {
23 | // Stubs out `import ... from 'three'` so it returns `import ... from window.THREE` effectively using THREE global variable that is defined by AFRAME.
24 | three: 'THREE'
25 | },
26 | plugins: [
27 | new MiniCssExtractPlugin({
28 | filename: '[name].css',
29 | chunkFilename: '[id].css'
30 | }),
31 | new Dotenv({
32 | path: './config/.env.development'
33 | })
34 | ],
35 | module: {
36 | rules: [
37 | {
38 | test: /\.jsx?$/,
39 | exclude: /node_modules/,
40 | use: {
41 | loader: 'babel-loader'
42 | }
43 | },
44 | {
45 | test: /\.svg$/,
46 | type: 'asset/inline'
47 | },
48 | {
49 | test: /\.css$/,
50 | use: ['style-loader', 'css-loader', 'postcss-loader']
51 | },
52 | {
53 | test: /\.module\.scss$/,
54 | use: [
55 | 'style-loader',
56 | {
57 | loader: 'css-loader',
58 | options: {
59 | modules: true,
60 | sourceMap: true
61 | }
62 | },
63 | {
64 | loader: 'sass-loader',
65 | options: {
66 | sourceMap: true
67 | }
68 | }
69 | ]
70 | },
71 | {
72 | test: /\.scss$/,
73 | exclude: /\.module\.scss$/,
74 | use: [
75 | 'style-loader',
76 | 'css-loader',
77 | {
78 | loader: 'sass-loader',
79 | options: {
80 | sourceMap: true
81 | }
82 | }
83 | ]
84 | },
85 | {
86 | test: /\.styl$/,
87 | exclude: /node_modules/,
88 | use: [
89 | 'style-loader',
90 | {
91 | loader: 'css-loader',
92 | options: { url: false }
93 | },
94 | {
95 | loader: 'postcss-loader',
96 | options: {
97 | postcssOptions: {
98 | plugins: ['autoprefixer']
99 | }
100 | }
101 | },
102 | 'stylus-loader'
103 | ]
104 | },
105 | {
106 | test: /\.(png|jpe?g|gif)$/i,
107 | use: [
108 | {
109 | loader: 'file-loader',
110 | options: {
111 | name: 'images/[name].[ext]'
112 | }
113 | }
114 | ]
115 | }
116 | ]
117 | }
118 | };
119 |
--------------------------------------------------------------------------------
/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const Dotenv = require('dotenv-webpack');
4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 |
7 | module.exports = {
8 | mode: 'production',
9 | entry: './src/index.js',
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: '3dstreet-editor.js',
13 | publicPath: '/'
14 | },
15 | plugins: [
16 | new CleanWebpackPlugin(),
17 | new MiniCssExtractPlugin({
18 | filename: '[name].[contenthash].css',
19 | chunkFilename: '[id].[contenthash].css'
20 | }),
21 | new Dotenv({
22 | path: './config/.env.production'
23 | }),
24 | new HtmlWebpackPlugin({
25 | template: '/public/index.html'
26 | })
27 | ],
28 | module: {
29 | rules: [
30 | {
31 | test: /\.jsx?$/,
32 | exclude: /node_modules/,
33 | use: {
34 | loader: 'babel-loader'
35 | }
36 | },
37 | {
38 | test: /\.svg$/,
39 | type: 'asset/inline'
40 | },
41 | {
42 | test: /\.css$/,
43 | use: ['style-loader', 'css-loader', 'postcss-loader']
44 | },
45 | {
46 | test: /\.module\.scss$/,
47 | use: [
48 | 'style-loader',
49 | {
50 | loader: 'css-loader',
51 | options: {
52 | modules: true,
53 | sourceMap: true
54 | }
55 | },
56 | {
57 | loader: 'sass-loader',
58 | options: {
59 | sourceMap: true
60 | }
61 | }
62 | ]
63 | },
64 | {
65 | test: /\.scss$/,
66 | exclude: /\.module\.scss$/,
67 | use: [
68 | 'style-loader',
69 | 'css-loader',
70 | {
71 | loader: 'sass-loader',
72 | options: {
73 | sourceMap: true
74 | }
75 | }
76 | ]
77 | },
78 | {
79 | test: /\.styl$/,
80 | exclude: /node_modules/,
81 | use: [
82 | 'style-loader',
83 | {
84 | loader: 'css-loader',
85 | options: { url: false }
86 | },
87 | {
88 | loader: 'postcss-loader',
89 | options: {
90 | postcssOptions: {
91 | plugins: ['autoprefixer']
92 | }
93 | }
94 | },
95 | 'stylus-loader'
96 | ]
97 | },
98 | {
99 | test: /\.(png|jpe?g|gif)$/i,
100 | use: [
101 | {
102 | loader: 'file-loader',
103 | options: {
104 | name: 'images/[name].[ext]'
105 | }
106 | }
107 | ]
108 | }
109 | ]
110 | },
111 | devtool: 'source-map'
112 | };
113 |
--------------------------------------------------------------------------------