From 667a56debdcb5a93cf9bdf31f2ec31d0fc98b779 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 22 Sep 2024 19:10:36 -0300 Subject: [PATCH 1/2] chore: add react package --- .github/workflows/react.ci.yml | 53 + packages/react/.eslintrc.json | 43 + packages/react/.gitignore | 26 + packages/react/.npmrc.ci | 2 + packages/react/.prettierrc | 8 + packages/react/.releaserc | 14 + packages/react/README.md | 93 ++ packages/react/eslint.config.mjs | 75 + packages/react/package.json | 80 + .../react/src/common/types/global.types.ts | 4 + .../src/components/autodesk/autodesk.tsx | 164 ++ .../src/components/autodesk/autodesk.types.ts | 41 + .../react/src/components/autodesk/index.ts | 2 + .../src/components/comments/comments.tsx | 54 + .../src/components/comments/comments.types.ts | 26 + .../react/src/components/comments/index.ts | 2 + .../form-elements/form-elements.tsx | 77 + .../form-elements/form-elements.types.ts | 32 + .../src/components/form-elements/index.ts | 2 + .../react/src/components/matterport/index.ts | 2 + .../src/components/matterport/matterport.tsx | 132 ++ .../components/matterport/matterport.types.ts | 27 + .../src/components/mouse-pointers/index.ts | 2 + .../mouse-pointers/mouse-pointers.tsx | 63 + .../mouse-pointers/mouse-pointers.types.ts | 10 + .../react/src/components/realtime/index.ts | 1 + .../src/components/realtime/realtime.tsx | 35 + .../src/components/realtime/realtime.types.ts | 7 + packages/react/src/components/three/index.ts | 2 + packages/react/src/components/three/three.tsx | 43 + .../react/src/components/three/three.types.ts | 16 + packages/react/src/components/video/index.ts | 2 + packages/react/src/components/video/video.tsx | 140 ++ .../react/src/components/video/video.types.ts | 77 + .../src/components/who-is-online/index.ts | 2 + .../who-is-online/who-is-online.tsx | 44 + .../who-is-online/who-is-online.types.ts | 14 + packages/react/src/contexts/room.tsx | 258 ++++ packages/react/src/contexts/room.types.ts | 79 + packages/react/src/demo.tsx | 71 + packages/react/src/hooks/useAutodesk.ts | 58 + packages/react/src/hooks/useAutodeskPin.ts | 50 + packages/react/src/hooks/useCanvasPin.ts | 57 + packages/react/src/hooks/useComments.ts | 68 + packages/react/src/hooks/useFormElements.ts | 124 ++ packages/react/src/hooks/useHtmlPin.ts | 51 + packages/react/src/hooks/useMatterport.ts | 66 + packages/react/src/hooks/useMatterportPin.ts | 52 + packages/react/src/hooks/useMouse.ts | 50 + packages/react/src/hooks/useRealtime.ts | 172 +++ .../react/src/hooks/useRealtimeParticipant.ts | 172 +++ packages/react/src/hooks/useSuperviz.ts | 16 + packages/react/src/hooks/useThree.ts | 57 + packages/react/src/hooks/useThreePin.ts | 62 + packages/react/src/hooks/useVideo.ts | 119 ++ packages/react/src/index.ts | 26 + packages/react/src/lib/sdk/index.ts | 51 + packages/react/src/main.tsx | 10 + packages/react/src/style.css | 124 ++ packages/react/src/utils/create-theme.ts | 35 + packages/react/src/vite-env.d.ts | 1 + packages/react/tsconfig.json | 31 + packages/react/tsconfig.node.json | 10 + packages/react/vite.config.ts | 48 + pnpm-lock.yaml | 1327 ++++++++++++++++- 65 files changed, 4661 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/react.ci.yml create mode 100644 packages/react/.eslintrc.json create mode 100644 packages/react/.gitignore create mode 100644 packages/react/.npmrc.ci create mode 100644 packages/react/.prettierrc create mode 100644 packages/react/.releaserc create mode 100644 packages/react/README.md create mode 100644 packages/react/eslint.config.mjs create mode 100644 packages/react/package.json create mode 100644 packages/react/src/common/types/global.types.ts create mode 100644 packages/react/src/components/autodesk/autodesk.tsx create mode 100644 packages/react/src/components/autodesk/autodesk.types.ts create mode 100644 packages/react/src/components/autodesk/index.ts create mode 100644 packages/react/src/components/comments/comments.tsx create mode 100644 packages/react/src/components/comments/comments.types.ts create mode 100644 packages/react/src/components/comments/index.ts create mode 100644 packages/react/src/components/form-elements/form-elements.tsx create mode 100644 packages/react/src/components/form-elements/form-elements.types.ts create mode 100644 packages/react/src/components/form-elements/index.ts create mode 100644 packages/react/src/components/matterport/index.ts create mode 100644 packages/react/src/components/matterport/matterport.tsx create mode 100644 packages/react/src/components/matterport/matterport.types.ts create mode 100644 packages/react/src/components/mouse-pointers/index.ts create mode 100644 packages/react/src/components/mouse-pointers/mouse-pointers.tsx create mode 100644 packages/react/src/components/mouse-pointers/mouse-pointers.types.ts create mode 100644 packages/react/src/components/realtime/index.ts create mode 100644 packages/react/src/components/realtime/realtime.tsx create mode 100644 packages/react/src/components/realtime/realtime.types.ts create mode 100644 packages/react/src/components/three/index.ts create mode 100644 packages/react/src/components/three/three.tsx create mode 100644 packages/react/src/components/three/three.types.ts create mode 100644 packages/react/src/components/video/index.ts create mode 100644 packages/react/src/components/video/video.tsx create mode 100644 packages/react/src/components/video/video.types.ts create mode 100644 packages/react/src/components/who-is-online/index.ts create mode 100644 packages/react/src/components/who-is-online/who-is-online.tsx create mode 100644 packages/react/src/components/who-is-online/who-is-online.types.ts create mode 100644 packages/react/src/contexts/room.tsx create mode 100644 packages/react/src/contexts/room.types.ts create mode 100644 packages/react/src/demo.tsx create mode 100644 packages/react/src/hooks/useAutodesk.ts create mode 100644 packages/react/src/hooks/useAutodeskPin.ts create mode 100644 packages/react/src/hooks/useCanvasPin.ts create mode 100644 packages/react/src/hooks/useComments.ts create mode 100644 packages/react/src/hooks/useFormElements.ts create mode 100644 packages/react/src/hooks/useHtmlPin.ts create mode 100644 packages/react/src/hooks/useMatterport.ts create mode 100644 packages/react/src/hooks/useMatterportPin.ts create mode 100644 packages/react/src/hooks/useMouse.ts create mode 100644 packages/react/src/hooks/useRealtime.ts create mode 100644 packages/react/src/hooks/useRealtimeParticipant.ts create mode 100644 packages/react/src/hooks/useSuperviz.ts create mode 100644 packages/react/src/hooks/useThree.ts create mode 100644 packages/react/src/hooks/useThreePin.ts create mode 100644 packages/react/src/hooks/useVideo.ts create mode 100644 packages/react/src/index.ts create mode 100644 packages/react/src/lib/sdk/index.ts create mode 100644 packages/react/src/main.tsx create mode 100644 packages/react/src/style.css create mode 100644 packages/react/src/utils/create-theme.ts create mode 100644 packages/react/src/vite-env.d.ts create mode 100644 packages/react/tsconfig.json create mode 100644 packages/react/tsconfig.node.json create mode 100644 packages/react/vite.config.ts diff --git a/.github/workflows/react.ci.yml b/.github/workflows/react.ci.yml new file mode 100644 index 00000000..03b31ea6 --- /dev/null +++ b/.github/workflows/react.ci.yml @@ -0,0 +1,53 @@ +name: React - Publish Package +on: + push: + branches: + - lab + - beta + - main + paths: + - 'packages/react/**' + - '.github/workflows/react.ci.yml' +jobs: + package: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] + steps: + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.10.0 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + env: + NPM_CONFIG_USERCONFIG: .npmrc.ci + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: git config --global user.name SuperViz + - run: git config --global user.email ci@superviz.com + - name: Publish npm package + run: npm whoami && pnpm run semantic-release --filter=@superviz/react-sdk + env: + NPM_CONFIG_USERCONFIG: .npmrc.ci + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + slack: + needs: package + name: Slack Notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_ICON: https://avatars.slack-edge.com/2020-11-18/1496892993975_af721d1c045bea2d5a46_48.png + MSG_MINIMAL: true + SLACK_USERNAME: Deploy react version ${{ github.ref_name }} \ No newline at end of file diff --git a/packages/react/.eslintrc.json b/packages/react/.eslintrc.json new file mode 100644 index 00000000..fcd74b78 --- /dev/null +++ b/packages/react/.eslintrc.json @@ -0,0 +1,43 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "tsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint", "react-hooks", "prettier","simple-import-sort"], + "rules": { + "camelcase": "error", + "no-duplicate-imports": "error", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any":"off", + "react/react-in-jsx-scope":"off", + "no-console": "off", + "no-alert": "error", + "react-hooks/exhaustive-deps": "off", + "react/prop-types": 0, + "react/display-name": 0, + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "@typescript-eslint/no-empty-function":"off", + "react/no-unknown-property":"off", + "react/no-unescaped-entities ":"off" + }, + "settings": { + "import/resolver": { + "typescript": {} + } + } + } \ No newline at end of file diff --git a/packages/react/.gitignore b/packages/react/.gitignore new file mode 100644 index 00000000..1d229540 --- /dev/null +++ b/packages/react/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +public/vendor +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.env \ No newline at end of file diff --git a/packages/react/.npmrc.ci b/packages/react/.npmrc.ci new file mode 100644 index 00000000..ec2ee41e --- /dev/null +++ b/packages/react/.npmrc.ci @@ -0,0 +1,2 @@ +@superviz:registry=https://registry.npmjs.org/ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} \ No newline at end of file diff --git a/packages/react/.prettierrc b/packages/react/.prettierrc new file mode 100644 index 00000000..d723adfb --- /dev/null +++ b/packages/react/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "all", + "arrowParens": "always", + "printWidth": 100, + "singleQuote": true, + "tabWidth": 2 +} diff --git a/packages/react/.releaserc b/packages/react/.releaserc new file mode 100644 index 00000000..15a72fc6 --- /dev/null +++ b/packages/react/.releaserc @@ -0,0 +1,14 @@ +{ + "branches": [ + "main", + { "name": "beta", "channel": "beta", "prerelease": true }, + { "name": "lab", "channel": "lab", "prerelease": true } + ], + "tagFormat": "@superviz/react/${version}", + "plugins": [ + "@semantic-release/commit-analyzer", + "semantic-release-version-file", + "@semantic-release/github", + "@semantic-release/npm" + ] +} diff --git a/packages/react/README.md b/packages/react/README.md new file mode 100644 index 00000000..4770d7f7 --- /dev/null +++ b/packages/react/README.md @@ -0,0 +1,93 @@ +

+ SuperViz Logo +

+ +

+ Discord + GitHub issues + GitHub pull requests + npm type definitions + Downloads +

+ +SuperViz provides a suite of programmable low-code Collaboration and Communication components, all synchronized with an advanced Real-time Data Engine, enabling real-time and asynchronous collaboration and communication within any JavaScript-based application. + +SuperViz offers a comprehensive suite of components, all synchronized with an advanced Real-time Data Engine, facilitating real-time collaboration in JavaScript-based applications. SuperViz SDK enables you to use one of our components: + +- Contextual Comments + - [Contextual Comments for HTML](https://docs.superviz.com/react-sdk/contextual-comments/HTML) + - [Contextual Comments for Canvas element](https://docs.superviz.com/react-sdk/contextual-comments/canvas) + - [Contextual Comments for Autodesk](https://docs.superviz.com/react-sdk/contextual-comments/autodesk) + - [Contextual Comments for Matterport](https://docs.superviz.com/react-sdk/contextual-comments/matterport) +- Presence + - [Real-time Mouse Pointers](https://docs.superviz.com/react-sdk/presence/mouse-pointers) + - [Real-time Data Engine](https://docs.superviz.com/react-sdk/presence/real-time-data-engine) + - [Who-is-Online](https://docs.superviz.com/react-sdk/presence/who-is-online) + - [Presence in Autodesk](https://docs.superviz.com/react-sdk/presence/AutodeskPresence) + - [Presence in Matterport](https://docs.superviz.com/react-sdk/presence/MatterportPresence) + - [Presence in ThreeJS](https://docs.superviz.com/react-sdk/presence/ThreeJsPresence) +- [Video Conference](https://docs.superviz.com/react-sdk/video/video-conference) + +You can also combine components to create a custom solution for your application. + +How to start coding with SuperViz? After installing this package, you’ll need to [create an account](https://dashboard.superviz.com/) to retrieve a SuperViz Token and start coding. + +## Quickstart + +### 1. Installation + +Install SuperViz SDK in your React app with the npm package: + +```bash +npm install --save @superviz/react-sdk +``` + +Or, with yarn: + +```bash +yarn add @superviz/react-sdk +``` + +### 2. Import the SDK + +Once installed, import the SDK to your code: + +```jsx +import { SuperVizRoomProvider } from "@superviz/react-sdk"; +``` + +### 3. Initialize the SDK + +After importing the SDK, you can initialize our provider by passing your `DEVELOPER_KEY` and important information about the participant. You can see details for the options object on the [React Initialization page](https://docs.superviz.com/react-sdk/initialization). + +The SuperVizRoomProvider is your primary gateway to access all SDK features, offering the essential methods to add its components. + +```jsx +", + name: "", + }} + participant={{ + id: "", + name: "", + }} + roomId=""> +

This is a room

+
+``` + +## Documentation + +You can find the complete documentation for every component and how to initialize them on the [SuperViz SDK Documentation page](https://docs.superviz.com/react-sdk/initialization). + +You can also find the complete changelog on the [Release Notes page](https://docs.superviz.com/releases). + +## Contributing + +If you are interested in contributing to SuperViz SDK, the best place to get involved with the community is through the [Discord server](https://discord.gg/weZ3Bfv6WZ), there you can find the latest news, ask questions, and share your experiences with SuperViz SDK. + +## License + +SuperViz SDK is licensed under the [BSD 2-Clause License](LICENSE). \ No newline at end of file diff --git a/packages/react/eslint.config.mjs b/packages/react/eslint.config.mjs new file mode 100644 index 00000000..ba968e3c --- /dev/null +++ b/packages/react/eslint.config.mjs @@ -0,0 +1,75 @@ +import react from "eslint-plugin-react"; +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import reactHooks from "eslint-plugin-react-hooks"; +import prettier from "eslint-plugin-prettier"; +import simpleImportSort from "eslint-plugin-simple-import-sort"; +import { fixupPluginRules } from "@eslint/compat"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [...compat.extends( + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "prettier", +), { + plugins: { + react, + "@typescript-eslint": typescriptEslint, + "react-hooks": fixupPluginRules(reactHooks), + prettier, + "simple-import-sort": simpleImportSort, + }, + + languageOptions: { + globals: { + ...globals.browser, + }, + + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module", + + parserOptions: { + ecmaFeatures: { + tsx: true, + }, + }, + }, + + settings: { + "import/resolver": { + typescript: {}, + }, + }, + + rules: { + camelcase: "error", + "no-duplicate-imports": "error", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + "react/react-in-jsx-scope": "off", + "no-console": "off", + "no-alert": "error", + "react-hooks/exhaustive-deps": "off", + "react/prop-types": 0, + "react/display-name": 0, + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "@typescript-eslint/no-empty-function": "off", + "react/no-unknown-property": "off", + "react/no-unescaped-entities ": "off", + }, +}]; \ No newline at end of file diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 00000000..da25acb4 --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,80 @@ +{ + "name": "@superviz/react-sdk", + "private": false, + "version": "0.0.0", + "type": "module", + "scripts": { + "watch": "tsc && vite build --watch", + "build": "tsc && vite build && cp package.json dist/package.json && cp README.md dist/README.md", + "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", + "lint:fix": "eslint --fix 'src/**/*.{jsx,ts,tsx}'", + "format": "prettier --write src//**/*.{ts,tsx,css} --config ./.prettierrc", + "semantic-release": "semantic-release", + "commit": "git-cz" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/superviz-sdk-react.es.js", + "require": "./dist/superviz-sdk-react.cjs.js", + "types": "./dist/index.d.ts" + }, + "./dist/style.css": "./dist/style.css" + }, + "main": "./dist/superviz-sdk-react.cjs.js", + "module": "./dist/superviz-sdk-react.es.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public", + "scope": "@superviz" + }, + "dependencies": { + "@superviz/autodesk-viewer-plugin": "workspace:*", + "@superviz/matterport-plugin": "workspace:*", + "@superviz/sdk": "workspace:*", + "@superviz/socket-client": "workspace:*", + "@superviz/threejs-plugin": "workspace:*", + "lodash": "^4.17.21", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@rollup/plugin-replace": "^5.0.7", + "@eslint/compat": "^1.1.1", + "@types/forge-viewer": "^7.89.1", + "@types/lodash": "^4.17.6", + "@types/node": "^20.14.9", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/three": "^0.166.0", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", + "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react-swc": "^3.7.0", + "eslint": "^9.6.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-simple-import-sort": "^12.1.0", + "glob": "^10.4.2", + "husky": "^9.0.11", + "lint-staged": "^15.2.7", + "prettier": "^3.3.2", + "react-hooks": "^1.0.1", + "semantic-release": "^24.0.0", + "semantic-release-version-file": "^1.0.2", + "typescript": "^5.5.2", + "vite": "^5.3.2", + "vite-plugin-dts": "^3.9.1", + "vite-plugin-linter": "^2.1.1", + "vite-tsconfig-paths": "^4.3.2" + } +} diff --git a/packages/react/src/common/types/global.types.ts b/packages/react/src/common/types/global.types.ts new file mode 100644 index 00000000..637dbb02 --- /dev/null +++ b/packages/react/src/common/types/global.types.ts @@ -0,0 +1,4 @@ +export type DefaultComponentProps = { + onMount?: () => void; + onUnmount?: () => void; +} & T; diff --git a/packages/react/src/components/autodesk/autodesk.tsx b/packages/react/src/components/autodesk/autodesk.tsx new file mode 100644 index 00000000..a4fc76b8 --- /dev/null +++ b/packages/react/src/components/autodesk/autodesk.tsx @@ -0,0 +1,164 @@ +import { Presence3D } from '@superviz/autodesk-viewer-plugin'; +import { useEffect, useState } from 'react'; +import { useInternalFeatures } from 'src/contexts/room'; +import type { AutoDeskComponent } from 'src/contexts/room.types'; + +import { AutodeskComponentProps, AutodeskViewerComponentProps } from './autodesk.types'; + +export function AutodeskPresence({ viewer, children, ...params }: AutodeskComponentProps) { + const { room, component, addComponent } = + useInternalFeatures('presence3dAutodesk'); + const [initializedTimestamp, setInitializedTimestamp] = useState(null); + + useEffect(() => { + if (!room || initializedTimestamp || !viewer) return; + + const autodesk = new Presence3D(viewer, params) as AutoDeskComponent; + + addComponent(autodesk); + setInitializedTimestamp(Date.now()); + }, [room, viewer]); + + useEffect(() => { + if (!component && initializedTimestamp) { + setInitializedTimestamp(null); + } + }, [component]); + + return children ?? <>; +} + +export function AutodeskViewer({ + modelUrn, + clientId, + clientSecret, + authUrl: url, + initializeOptions, + data, + onDocumentLoadError, + onDocumentLoadSuccess, + onViewerInitialized, + isAvatarsEnabled, + isLaserEnabled, + isNameEnabled, + isMouseEnabled, + avatarConfig, + ...divParams +}: AutodeskViewerComponentProps) { + const [instance, setInstance] = useState(null); + const { hasJoinedRoom } = useInternalFeatures('presence3dAutodesk'); + const authUrl = url || 'https://developer.api.autodesk.com/authentication/v2/token'; + const token = btoa(`${clientId}:${clientSecret}`); + const body = { + // eslint-disable-next-line camelcase + grant_type: 'client_credentials', + scope: 'data:read bucket:read', + ...data, + }; + + useEffect(() => { + if (hasJoinedRoom) { + loadDocument(); + return; + } + + if (instance) { + instance.finish(); + setInstance(null); + } + }, [hasJoinedRoom]); + + async function loadDocument() { + let viewer: Autodesk.Viewing.GuiViewer3D | null = null; + + if (!window.Autodesk) { + console.error('[Superviz] Autodesk not found'); + return; + } + + await fetch(authUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${token}`, + }, + body: new URLSearchParams(body).toString(), + }) + .then((response) => response.json()) + .then((dataToken) => { + const modelId = btoa(modelUrn); + const documentId = `urn:${modelId}`; + + const options = { + env: 'AutodeskProduction2', + api: 'streamingV2', + accessToken: dataToken.access_token, + ...initializeOptions, + }; + + window.Autodesk.Viewing.Initializer(options, async () => { + const container = document.getElementById('autodesk-content'); + + if (!container) { + console.error('[Superviz] Autodesk container not found'); + return; + } + + viewer = new window.Autodesk.Viewing.GuiViewer3D(container); + await viewer.start(); + + onViewerInitialized && + onViewerInitialized({ + viewer, + container, + }); + + window.Autodesk.Viewing.Document.load(documentId, onLoadSuccess, onLoadFailure); + }); + }); + + function onLoadSuccess(doc: any) { + if (onDocumentLoadSuccess) onDocumentLoadSuccess(doc); + + const viewable = doc.getRoot().getDefaultGeometry(); + if (!viewable || !viewer) return; + + const options = { + applyScaling: 'meters', + }; + + viewer + .loadDocumentNode(doc, viewable, options) + .then(() => { + if (!viewer) return; + setInstance(viewer); + }) + .catch((error) => { + console.error('[SuperViz] Failed to load document node', error); + }); + } + + function onLoadFailure( + errorCode: Autodesk.Viewing.ErrorCodes, + errorMsg: string, + messages: any[], + ) { + if (onDocumentLoadError) onDocumentLoadError(errorCode, errorMsg, messages); + + console.error('[SuperViz] Failed to load document', errorCode, errorMsg, messages); + } + } + + return ( + +
+
+ ); +} diff --git a/packages/react/src/components/autodesk/autodesk.types.ts b/packages/react/src/components/autodesk/autodesk.types.ts new file mode 100644 index 00000000..9ad7868f --- /dev/null +++ b/packages/react/src/components/autodesk/autodesk.types.ts @@ -0,0 +1,41 @@ +import { AvatarConfig } from '@superviz/autodesk-viewer-plugin/dist/types'; +import type { ReactElement } from 'react'; + +export type AutodeskComponentProps = { + viewer: Autodesk.Viewing.GuiViewer3D; + children?: ReactElement | string | ReactElement[] | null; + isAvatarsEnabled?: boolean; + isLaserEnabled?: boolean; + isNameEnabled?: boolean; + isMouseEnabled?: boolean; + avatarConfig?: AvatarConfig; +}; + +type ViewerInitializedParams = { + viewer: Autodesk.Viewing.GuiViewer3D; + container: HTMLElement; +}; + +export type AutodeskViewerComponentProps = { + modelUrn: string; + clientId: string; + clientSecret: string; + + onViewerInitialized?: (params: ViewerInitializedParams) => void; + onDocumentLoadSuccess?: (doc: Document) => void; + onDocumentLoadError?: ( + errorCode: Autodesk.Viewing.ErrorCodes, + errorMsg: string, + messages: any[], + ) => void; + + authUrl?: string; + initializeOptions?: Omit; + data?: { + client_id?: string; + client_secret?: string; + grant_type: string; + scope: string; + }; +} & Omit & + Omit, 'id'>; diff --git a/packages/react/src/components/autodesk/index.ts b/packages/react/src/components/autodesk/index.ts new file mode 100644 index 00000000..9e2a8dbf --- /dev/null +++ b/packages/react/src/components/autodesk/index.ts @@ -0,0 +1,2 @@ +export * from './autodesk'; +export * from './autodesk.types'; diff --git a/packages/react/src/components/comments/comments.tsx b/packages/react/src/components/comments/comments.tsx new file mode 100644 index 00000000..a29d5e1a --- /dev/null +++ b/packages/react/src/components/comments/comments.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react'; +import { useInternalFeatures } from 'src/contexts/room'; + +import { CommentsComponent } from '../../lib/sdk'; +import { CommentsProps } from './comments.types'; + +export function Comments({ + pin, + children, + onPinActive, + onPinInactive, + onMount, + onUnmount, + ...params +}: CommentsProps) { + const { room, component, addComponent } = useInternalFeatures('comments'); + const [initializedTimestamp, setInitializedTimestamp] = useState(null); + + const callbacks = { + 'pin-mode.active': onPinActive, + 'pin-mode.inactive': onPinInactive, + mount: onMount, + unmount: onUnmount, + }; + + useEffect(() => { + if (!component) return; + + Object.entries(callbacks).forEach(([event, callback]) => { + component.unsubscribe(event, callback); + + if (callback) { + component.subscribe(event, callback); + } + }); + }, [component, room, onPinActive, onPinInactive, onMount, onUnmount]); + + useEffect(() => { + if (!room || initializedTimestamp || !pin) return; + + const commentsInstance = new CommentsComponent(pin, { ...params }); + + addComponent(commentsInstance); + setInitializedTimestamp(Date.now()); + }, [room, pin]); + + useEffect(() => { + if (!component && initializedTimestamp) { + setInitializedTimestamp(null); + } + }, [component]); + + return children ?? <>; +} diff --git a/packages/react/src/components/comments/comments.types.ts b/packages/react/src/components/comments/comments.types.ts new file mode 100644 index 00000000..e06d3f13 --- /dev/null +++ b/packages/react/src/components/comments/comments.types.ts @@ -0,0 +1,26 @@ +import type { AutodeskPin } from '@superviz/autodesk-viewer-plugin'; +import type { MatterportPin } from '@superviz/matterport-plugin'; +import type { ThreeJsPin } from '@superviz/threejs-plugin'; +import { ReactElement } from 'react'; +import { DefaultComponentProps } from 'src/common/types/global.types'; + +import type { ButtonLocation, CanvasPin, CommentsSide, HTMLPin } from '../../lib/sdk'; + +export type CommentsProps = DefaultComponentProps<{ + pin: CanvasPin | HTMLPin | MatterportPin | ThreeJsPin | AutodeskPin | null; + children?: ReactElement | string | ReactElement[] | null; + + onPinActive?: () => void; + onPinInactive?: () => void; + + position?: `${CommentsSide}`; + buttonLocation?: `${ButtonLocation}` | string; + hideDefaultButton?: boolean; + styles?: string; + offset?: { + left?: number; + top?: number; + right?: number; + bottom?: number; + }; +}>; diff --git a/packages/react/src/components/comments/index.ts b/packages/react/src/components/comments/index.ts new file mode 100644 index 00000000..e5479a30 --- /dev/null +++ b/packages/react/src/components/comments/index.ts @@ -0,0 +1,2 @@ +export * from './comments'; +export * from './comments.types'; diff --git a/packages/react/src/components/form-elements/form-elements.tsx b/packages/react/src/components/form-elements/form-elements.tsx new file mode 100644 index 00000000..7954a8bf --- /dev/null +++ b/packages/react/src/components/form-elements/form-elements.tsx @@ -0,0 +1,77 @@ +import { useEffect, useState } from 'react'; +import { useInternalFeatures } from 'src/contexts/room'; + +import { ComponentLifeCycleEvent, FormElementsComponent } from '../../lib/sdk'; +import { FieldEvents, FormElementsProps } from './form-elements.types'; + +export function FormElements({ + onMount, + onUnmount, + disableOutline, + disableRealtimeSync, + fields, + children, + onInteraction, + onContentChange, +}: FormElementsProps) { + const { room, component, addComponent } = + useInternalFeatures('formElements'); + const [initializedTimestamp, setInitializedTimestamp] = useState(null); + + useEffect(() => { + if (!component) return; + component.unsubscribe(ComponentLifeCycleEvent.MOUNT); + + if (onMount) { + component.subscribe(ComponentLifeCycleEvent.MOUNT, onMount); + } + }, [component, onMount]); + + useEffect(() => { + if (!component) return; + component.unsubscribe(ComponentLifeCycleEvent.UNMOUNT); + + if (onUnmount) { + component.subscribe(ComponentLifeCycleEvent.UNMOUNT, onUnmount); + } + }, [component, onUnmount]); + + useEffect(() => { + if (!component) return; + component.unsubscribe(FieldEvents.CONTENT_CHANGE); + + if (onContentChange) { + component.subscribe(FieldEvents.CONTENT_CHANGE, onContentChange); + } + }, [component, onContentChange]); + + useEffect(() => { + if (!component) return; + component.unsubscribe(FieldEvents.INTERACTION); + + if (onInteraction) { + component.subscribe(FieldEvents.INTERACTION, onInteraction); + } + }, [component, onInteraction]); + + useEffect(() => { + if (!room || initializedTimestamp) return; + + const formElementsInstance = new FormElementsComponent({ + fields, + disableOutline, + disableRealtimeSync, + }); + + addComponent(formElementsInstance); + setInitializedTimestamp(Date.now()); + }, [room]); + + useEffect(() => { + if (!component && initializedTimestamp) { + setInitializedTimestamp(null); + } + }, [component]); + + return children ?? <>; +} diff --git a/packages/react/src/components/form-elements/form-elements.types.ts b/packages/react/src/components/form-elements/form-elements.types.ts new file mode 100644 index 00000000..d7acc090 --- /dev/null +++ b/packages/react/src/components/form-elements/form-elements.types.ts @@ -0,0 +1,32 @@ +import { ReactElement } from 'react'; +import { DefaultComponentProps } from 'src/common/types/global.types'; + +export type FormElementsProps = DefaultComponentProps<{ + children?: ReactElement | string | ReactElement[] | null; + fields?: string[] | string; + disableOutline?: boolean; + disableRealtimeSync?: boolean; + onContentChange?: (data: { + value: string; + fieldId: string; + attribute: string; + userId: string; + userName: string; + timestamp: number; + }) => void; + onInteraction?: (data: { + fieldId: string; + userId: string; + userName: string; + color: string; + }) => void; +}>; + +export enum FieldEvents { + BLUR = 'field.blur', + FOCUS = 'field.focus', + CONTENT_CHANGE = 'field.content-change', + INTERACTION = 'field.interaction', +} + +export type Field = HTMLInputElement | HTMLTextAreaElement; diff --git a/packages/react/src/components/form-elements/index.ts b/packages/react/src/components/form-elements/index.ts new file mode 100644 index 00000000..d9369cca --- /dev/null +++ b/packages/react/src/components/form-elements/index.ts @@ -0,0 +1,2 @@ +export * from './form-elements'; +export * from './form-elements.types'; diff --git a/packages/react/src/components/matterport/index.ts b/packages/react/src/components/matterport/index.ts new file mode 100644 index 00000000..ce86529b --- /dev/null +++ b/packages/react/src/components/matterport/index.ts @@ -0,0 +1,2 @@ +export * from './matterport'; +export * from './matterport.types'; diff --git a/packages/react/src/components/matterport/matterport.tsx b/packages/react/src/components/matterport/matterport.tsx new file mode 100644 index 00000000..8631efb9 --- /dev/null +++ b/packages/react/src/components/matterport/matterport.tsx @@ -0,0 +1,132 @@ +import { Presence3D } from '@superviz/matterport-plugin'; +import type { MpSdk } from '@superviz/matterport-plugin/dist/common/types/matterport.types'; +import type { MatterportComponentOptions } from '@superviz/matterport-plugin/dist/types'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useInternalFeatures } from 'src/contexts/room'; +import { MatterportComponent } from 'src/contexts/room.types'; + +import { MatterportComponentProps, MatterportIframeProps } from './matterport.types'; + +export function MatterportPresence({ + matterportSdkInstance, + children, + ...params +}: MatterportComponentProps) { + const { room, component, addComponent, removeComponent } = + useInternalFeatures('presence3dMatterport'); + const [initializedTimestamp, setInitializedTimestamp] = useState(null); + + useEffect(() => { + if (!room || !matterportSdkInstance) { + return; + } + + const matterportInstance = new Presence3D( + matterportSdkInstance, + params as MatterportComponentOptions, + ) as MatterportComponent; + + addComponent(matterportInstance); + setInitializedTimestamp(Date.now()); + }, [room, matterportSdkInstance]); + + useEffect(() => { + if (!component && initializedTimestamp) { + setInitializedTimestamp(null); + } + }, [component]); + + useEffect(() => { + if ((!matterportSdkInstance || !room) && component) { + removeComponent(component); + } + }, [matterportSdkInstance, room]); + + return children ?? <>; +} + +export function MatterportIframe({ + isAvatarsEnabled, + isLaserEnabled, + isNameEnabled, + avatarConfig, + bundleUrl, + matterportKey, + onMpSdkLoaded, + ...iframeProps +}: MatterportIframeProps) { + const { room, removeComponent, component } = + useInternalFeatures('presence3dMatterport'); + const iframe = useRef(null); + const [matterportInstance, setMatterportInstance] = useState(null); + const url = useRef(''); + + useEffect(() => { + if (!iframe.current) return; + + initializeMatterportSdk(); + }, [bundleUrl, iframe.current]); + + useEffect(() => { + if (!room && matterportInstance) { + setMatterportInstance(null); + iframe.current?.remove(); + } + }, [room]); + + const initializeMatterportSdk = useCallback(async () => { + if (url.current === bundleUrl) { + return; + } + url.current = bundleUrl; + + if (matterportInstance) { + setMatterportInstance(null); + removeComponent(component); + } + + const showcase = document.getElementById('showcase') as HTMLIFrameElement; + + const showcaseWindow = showcase.contentWindow; + showcase.setAttribute('src', bundleUrl); + + if (!showcaseWindow) { + console.error('[SuperViz] Matterport Showcase iframe not found'); + return; + } + + const mpsdk = await new Promise((resolve) => { + const callback = async () => { + // @ts-ignore + const mpSdk = (await showcaseWindow.MP_SDK.connect(showcaseWindow, matterportKey)) as MpSdk; + + if (onMpSdkLoaded) { + onMpSdkLoaded({ + matterportSdkInstance: mpSdk, + showcaseWindow: iframe.current as HTMLIFrameElement, + }); + } + + resolve(mpSdk); + showcase.removeEventListener('load', callback); + }; + + showcase.addEventListener('load', callback); + }); + + setMatterportInstance(mpsdk); + }, [bundleUrl]); + + return ( + <> + +