diff --git a/samples/javascript/react_sample/.gitignore b/samples/javascript/react_sample/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/samples/javascript/react_sample/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/samples/javascript/react_sample/README.md b/samples/javascript/react_sample/README.md new file mode 100644 index 000000000..23d5d4e6d --- /dev/null +++ b/samples/javascript/react_sample/README.md @@ -0,0 +1,16 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## What changes from the original create-react-app +1. Adding server.js using Express to serve the client app and providing a `negotiate` endpoint for the client to get the `Client Access URI` +1. adding a npm command `npm run server` to build the client app and running the Expressserver +1. update src/App.js to create a WebSocket connection + + +## How to run +``` +export WebPubSubConnectionString=”Your_Connection_String” +npm install +npm run server +``` \ No newline at end of file diff --git a/samples/javascript/react_sample/package.json b/samples/javascript/react_sample/package.json new file mode 100644 index 000000000..6394cb653 --- /dev/null +++ b/samples/javascript/react_sample/package.json @@ -0,0 +1,41 @@ +{ + "name": "sample-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@azure/web-pubsub": "^1.1.1", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "express": "^4.18.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "server": "npm run build && node server.js", + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/samples/javascript/react_sample/public/favicon.ico b/samples/javascript/react_sample/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/samples/javascript/react_sample/public/favicon.ico differ diff --git a/samples/javascript/react_sample/public/index.html b/samples/javascript/react_sample/public/index.html new file mode 100644 index 000000000..aa069f27c --- /dev/null +++ b/samples/javascript/react_sample/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/samples/javascript/react_sample/public/logo192.png b/samples/javascript/react_sample/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/samples/javascript/react_sample/public/logo192.png differ diff --git a/samples/javascript/react_sample/public/logo512.png b/samples/javascript/react_sample/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/samples/javascript/react_sample/public/logo512.png differ diff --git a/samples/javascript/react_sample/public/manifest.json b/samples/javascript/react_sample/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/samples/javascript/react_sample/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/samples/javascript/react_sample/public/robots.txt b/samples/javascript/react_sample/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/samples/javascript/react_sample/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/samples/javascript/react_sample/server.js b/samples/javascript/react_sample/server.js new file mode 100644 index 000000000..d56f1c3f9 --- /dev/null +++ b/samples/javascript/react_sample/server.js @@ -0,0 +1,24 @@ +const express = require("express"); +const { WebPubSubServiceClient } = require("@azure/web-pubsub"); + +const app = express(); +const hubName = "hubtest1"; +const port = 8080; + +let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName); + +app.get("/negotiate", async (req, res) => { + // this is a super simple demo for how to do auth, normally you go through an auth flow + let id = req.query.id; + if (!id) { + res.status(400).send("missing user id"); + return; + } + let token = await serviceClient.getClientAccessToken({ userId: id }); + res.json({ + url: token.url, + }); +}); + +app.use(express.static("build")); +app.listen(port, () => console.log("server started at localhost:" + port)); diff --git a/samples/javascript/react_sample/src/App.css b/samples/javascript/react_sample/src/App.css new file mode 100644 index 000000000..74b5e0534 --- /dev/null +++ b/samples/javascript/react_sample/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/samples/javascript/react_sample/src/App.js b/samples/javascript/react_sample/src/App.js new file mode 100644 index 000000000..e5d3272ef --- /dev/null +++ b/samples/javascript/react_sample/src/App.js @@ -0,0 +1,54 @@ +import logo from "./logo.svg"; +import "./App.css"; +import { useEffect, useState } from "react"; + +function App() { + const [data, setData] = useState("Connecting"); + + useEffect(() => { + let socket; + + async function createConnection() { + try { + const response = await fetch("/negotiate?id=userId1"); + const token = await response.json(); + + socket = new WebSocket(token.url); + + socket.onopen = () => { + console.log("WebSocket connected"); + setData("Connected"); + }; + + socket.onclose = (event) => { + console.log("WebSocket closed:", event); + setData("Connection closed"); + }; + } catch (error) { + console.error("Error creating WebSocket connection:", error); + setData("Connection error"); + } + } + + createConnection(); + + // Clean up the WebSocket connection when the component unmounts + return () => { + if (socket) { + socket.onclose = null; // Remove the onclose handler to avoid memory leaks + socket.close(); + } + }; + }, []); // Empty dependency array means this effect runs once when the component mounts + + return ( +
+
+ logo +

{data}

+
+
+ ); +} + +export default App; diff --git a/samples/javascript/react_sample/src/App.test.js b/samples/javascript/react_sample/src/App.test.js new file mode 100644 index 000000000..1f03afeec --- /dev/null +++ b/samples/javascript/react_sample/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/samples/javascript/react_sample/src/index.css b/samples/javascript/react_sample/src/index.css new file mode 100644 index 000000000..ec2585e8c --- /dev/null +++ b/samples/javascript/react_sample/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/samples/javascript/react_sample/src/index.js b/samples/javascript/react_sample/src/index.js new file mode 100644 index 000000000..770ee7d65 --- /dev/null +++ b/samples/javascript/react_sample/src/index.js @@ -0,0 +1,17 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/samples/javascript/react_sample/src/logo.svg b/samples/javascript/react_sample/src/logo.svg new file mode 100644 index 000000000..9dfc1c058 --- /dev/null +++ b/samples/javascript/react_sample/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/javascript/react_sample/src/reportWebVitals.js b/samples/javascript/react_sample/src/reportWebVitals.js new file mode 100644 index 000000000..5253d3ad9 --- /dev/null +++ b/samples/javascript/react_sample/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/samples/javascript/react_sample/src/setupTests.js b/samples/javascript/react_sample/src/setupTests.js new file mode 100644 index 000000000..8f2609b7b --- /dev/null +++ b/samples/javascript/react_sample/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom';