├── public └── flush.mp3 ├── vue.config.js ├── api ├── .funcignore ├── local.settings.json.example ├── package.json ├── host.json ├── createTokenRequest │ ├── function.json │ └── index.js └── .gitignore ├── agileflush_screenshot.png ├── diagrams ├── agile-flush-functionality.png ├── aglile-flush-main-technical-components.png ├── components.mmd ├── handle-show-results-received.mmd ├── handle-newparticipant-entered.mmd ├── handle-vote-received.mmd ├── toggle-show-results.mmd ├── handle-reset-voting-received.mmd ├── functionality.mmd ├── reset-votes.mmd ├── do-voting.mmd ├── README.md ├── start-session.mmd ├── handle-show-results-received.svg ├── handle-newparticipant-entered.svg ├── handle-vote-received.svg ├── handle-reset-voting-received.svg ├── toggle-show-results.svg ├── reset-votes.svg ├── components.svg ├── do-voting.svg ├── functionality.svg └── start-session.svg ├── prettier.config.js ├── vite.config.js ├── staticwebapp.config.json ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── src ├── .editorconfig ├── main.js ├── router │ └── index.js ├── components │ ├── FooterSection.vue │ ├── SessionSection.vue │ ├── HomePage.vue │ └── CardItem.vue ├── App.vue └── store │ ├── index.js │ ├── cardModule.js │ └── realtimeModule.js ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .tours ├── start.tour ├── folder-structure.tour ├── azure-functions-app.tour └── vue-app.tour ├── swa-cli.config.json ├── .github ├── dependabot.yml └── workflows │ ├── codetourchecker.yml │ ├── linkchecker.yml │ └── azure-static-web-apps-gentle-moss-08d9e3303.yml ├── .eslintrc.js ├── index.html ├── SECURITY.md ├── package.json ├── .gitignore ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE /public/flush.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/agile-flush-vue-app/HEAD/public/flush.mp3 -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configureWebpack: { 3 | devtool: 'source-map', 4 | }, 5 | } -------------------------------------------------------------------------------- /api/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /agileflush_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/agile-flush-vue-app/HEAD/agileflush_screenshot.png -------------------------------------------------------------------------------- /diagrams/agile-flush-functionality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/agile-flush-vue-app/HEAD/diagrams/agile-flush-functionality.png -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "none", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: false, 6 | printWidth: 130 7 | }; 8 | -------------------------------------------------------------------------------- /diagrams/aglile-flush-main-technical-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/agile-flush-vue-app/HEAD/diagrams/aglile-flush-main-technical-components.png -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) -------------------------------------------------------------------------------- /staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/api/*", 5 | "methods": ["GET"] 6 | } 7 | ], 8 | "navigationFallback": { 9 | "rewrite": "/index.html" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-azuretools.vscode-azurestaticwebapps", 5 | "dbaeumer.vscode-eslint", 6 | "vsls-contrib.codetour" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /api/local.settings.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "node", 6 | "ABLY_API_KEY": "" 7 | } 8 | } -------------------------------------------------------------------------------- /diagrams/components.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | subgraph ably [Ably] 3 | step2[Ably app] 4 | end 5 | subgraph azure [Azure Static Web Apps] 6 | step1[Vue.js application]-->step3 7 | step3[Node.js Azure Function]-->step2 8 | end 9 | step1 <--channel--> step2 -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { router } from './router'; 3 | import { store } from './store'; 4 | import App from './App.vue'; 5 | 6 | const app = createApp(App); 7 | app.use(router); 8 | app.use(store); 9 | router.isReady().then(() => app.mount('#app')); 10 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "func start", 7 | "test": "echo \"No tests yet...\"" 8 | }, 9 | "dependencies": { 10 | "ably": "^1.2.15" 11 | }, 12 | "devDependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /diagrams/handle-show-results-received.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | Ably->>Vuex: broadcast: show-results 3 | activate Vuex 4 | Vuex->>Vuex: handleShowResultsReceived(bool) 5 | Vuex->>Vuex: setShowResults(bool) 6 | Client->>Vuex: showResults() 7 | Vuex-->>Client: bool 8 | deactivate Vuex 9 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import App from '../App.vue'; 3 | 4 | export const router = createRouter({ 5 | history: createWebHistory(), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'home', 10 | component: App, 11 | }, 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Append -bullseye variants on local arm64/Apple Silicon. 2 | 3 | ARG VARIANT=16 4 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:${VARIANT} 5 | 6 | RUN su node -c "npm install -g npm" 7 | RUN su node -c "npm install -g azure-functions-core-tools@4" 8 | RUN su node -c "npm install -g @azure/static-web-apps-cli" 9 | -------------------------------------------------------------------------------- /api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /diagrams/handle-newparticipant-entered.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | Ably->>Vuex: broadcast: participant entered 3 | activate Vuex 4 | Vuex->>Vuex: handleNewParticipantEntered(participant) 5 | Vuex->>Vuex: addParticipantJoined(clientId) 6 | Client->>Vuex: numberOfParticipantsJoined() 7 | Vuex-->>Client: count 8 | 9 | deactivate Vuex 10 | -------------------------------------------------------------------------------- /api/createTokenRequest/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get" 10 | ] 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.useIgnoreFiles": false, 3 | "azureFunctions.deploySubpath": "api", 4 | "azureFunctions.postDeployTask": "npm install (functions)", 5 | "azureFunctions.projectLanguage": "JavaScript", 6 | "azureFunctions.projectRuntime": "~3", 7 | "debug.internalConsoleOptions": "neverOpen", 8 | "azureFunctions.preDeployTask": "npm prune (functions)" 9 | } -------------------------------------------------------------------------------- /diagrams/handle-vote-received.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | Ably->>Vuex: broadcast: vote 3 | activate Vuex 4 | Vuex->>Vuex: handleVoteReceived(vote) 5 | Vuex->>Vuex: addParticipantVoted(vote) 6 | Client->>Vuex: voteCountForCard(cardNumber) 7 | Vuex-->>Client: count 8 | Client->>Vuex: numberOfParticipantsVoted() 9 | Vuex-->>Client: count 10 | deactivate Vuex 11 | -------------------------------------------------------------------------------- /.tours/start.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "1 - Start", 4 | "steps": [ 5 | { 6 | "title": "Introduction", 7 | "description": "This CodeTour will guide you through this repository and explain how the Agile Flush application is structured.\n\n* [Folder Structure]\n* [Azure Functions App]\n* [Vue App]" 8 | } 9 | ], 10 | "isPrimary": true 11 | } -------------------------------------------------------------------------------- /diagrams/toggle-show-results.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | Client->>Vuex: toggleShowResults() 3 | activate Client 4 | activate Vuex 5 | Vuex->>Vuex: toggleShowResults() 6 | Vuex->>Ably: publish(show-results) 7 | activate Ably 8 | Ably->>Other Clients: broadcast: show-results 9 | deactivate Ably 10 | Client->>Vuex: showResults() 11 | Vuex-->>Client: bool 12 | deactivate Vuex -------------------------------------------------------------------------------- /diagrams/handle-reset-voting-received.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | Ably->>Vuex: broadcast: reset-voting 3 | activate Vuex 4 | Vuex->>Vuex: handleResetVotingReceived(vote) 5 | Vuex->>Vuex: commonResetVoting(vote) 6 | Client->>Vuex: voteCountForCard(cardNumber) 7 | Vuex-->>Client: count 8 | Client->>Vuex: numberOfParticipantsVoted() 9 | Vuex-->>Client: count 10 | 11 | deactivate Vuex 12 | -------------------------------------------------------------------------------- /diagrams/functionality.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | step1[Host visits website]-->step2 3 | step2[Start session]-->step3 4 | step3[Send URL to team members]-->voting 5 | step4[Team member visits URL]-->voting 6 | subgraph voting [Planning Session] 7 | step5[Select card]-->step5 & step7 8 | step6[Show/hide votes]-->step7 9 | step7{Votes visible?}--Yes-->step8 10 | step8[Reset votes]-->step5 11 | end -------------------------------------------------------------------------------- /swa-cli.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", 3 | "configurations": { 4 | "agile-flush-vue-app": { 5 | "appLocation": ".", 6 | "apiLocation": "api", 7 | "outputLocation": "dist", 8 | "appBuildCommand": "npm run build", 9 | "apiBuildCommand": "npm run build --if-present", 10 | "run": "npm run dev", 11 | "appDevserverUrl": "http://localhost:5173" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /diagrams/reset-votes.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | Client->>Vuex: resetVoting() 3 | activate Client 4 | activate Vuex 5 | Vuex->>Vuex: commonResetVoting() 6 | Vuex->>Ably: publish(reset-voting) 7 | activate Ably 8 | Ably->>Other Clients: broadcast: reset-voting 9 | deactivate Ably 10 | Client->>Vuex: voteCountForCard(cardNumber) 11 | Vuex-->>Client: count 12 | Client->>Vuex: numberOfParticipantsVoted() 13 | Vuex-->>Client: count 14 | deactivate Vuex -------------------------------------------------------------------------------- /api/createTokenRequest/index.js: -------------------------------------------------------------------------------- 1 | const Ably = require('ably/promises'); 2 | 3 | module.exports = async function (context) { 4 | const id = `id- + ${Math.random().toString(36).substr(2, 16)}`; 5 | const client = new Ably.Realtime(process.env.ABLY_API_KEY); 6 | const tokenRequestData = await client.auth.createTokenRequest({ clientId: id }); 7 | context.res = { 8 | headers: { "content-type": "application/json" }, 9 | body: JSON.stringify(tokenRequestData) 10 | }; 11 | }; -------------------------------------------------------------------------------- /diagrams/do-voting.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | Client->>Vuex: selectCard(cardNumber) 3 | activate Client 4 | activate Vuex 5 | Vuex->>Vuex: doVote(cardNumber) 6 | Vuex->>Vuex: addParticipantVoted(vote) 7 | Vuex->>Ably: publish(vote) 8 | activate Ably 9 | Ably->>Other Clients: broadcast: vote 10 | deactivate Ably 11 | Client->>Vuex: voteCountForCard(cardNumber) 12 | Vuex-->>Client: count 13 | Client->>Vuex: numberOfParticipantsVoted() 14 | Vuex-->>Client: count 15 | deactivate Vuex -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: 'npm' 5 | directory: '/' 6 | schedule: 7 | interval: 'daily' 8 | time: '6:00' 9 | reviewers: 10 | - 'marcduiker' 11 | 12 | - package-ecosystem: 'npm' 13 | directory: '/api' 14 | schedule: 15 | interval: 'daily' 16 | time: '6:00' 17 | reviewers: 18 | - 'marcduiker' 19 | 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "daily" 24 | time: '6:00' 25 | reviewers: 26 | - 'marcduiker' 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "plugin:vue/essential", 9 | "standard", 10 | "prettier", 11 | "plugin:vue/vue3-recommended", 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 13, 15 | sourceType: "module", 16 | }, 17 | plugins: [ 18 | "vue", 19 | ], 20 | rules: { 21 | "arrow-body-style": "off", 22 | "no-underscore-dangle": "off", 23 | "no-plusplus": "off", 24 | "no-param-reassign": "off", 25 | "import/prefer-default-export" : "off" 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-node-watch", 8 | "isBackground": true, 9 | "dependsOn": "npm install (functions)", 10 | "options": { 11 | "cwd": "${workspaceFolder}/api" 12 | } 13 | }, 14 | { 15 | "type": "shell", 16 | "label": "npm run serve", 17 | "command": "npm run serve" 18 | }, 19 | { 20 | "type": "shell", 21 | "label": "npm prune (functions)", 22 | "command": "npm prune --production", 23 | "problemMatcher": [], 24 | "options": { 25 | "cwd": "${workspaceFolder}/api" 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.github/workflows/codetourchecker.yml: -------------------------------------------------------------------------------- 1 | name: CodeTour Checker 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize, reopened] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | 12 | jobs: 13 | codetour-checker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: 'Checkout source code' 17 | uses: actions/checkout@v2 18 | 19 | - name: 'Watch CodeTour changes' 20 | uses: pozil/codetour-watch@v1.3.0 21 | with: 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | tour-path: '.tours/' 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Agile Flush 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/FooterSection.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /diagrams/README.md: -------------------------------------------------------------------------------- 1 | # Sequence Diagrams 2 | 3 | ## Start session 4 | 5 | ![Start session](start-session.svg) 6 | 7 | ## Handle new participant entered 8 | 9 | ![Handle new participant entered](handle-newparticipant-entered.svg) 10 | 11 | ## Do vote 12 | 13 | ![Do vote](do-voting.svg) 14 | 15 | ## Handle vote received 16 | 17 | ![Handle vote received](handle-vote-received.svg) 18 | 19 | ## Reset voting 20 | 21 | ![Reset voting](reset-votes.svg) 22 | 23 | ## Handle reset voting received 24 | 25 | ![Handle show results received](handle-reset-voting-received.svg) 26 | 27 | ## Toggle show results 28 | 29 | ![Toggle show results](toggle-show-results.svg) 30 | 31 | ## Handle show results received 32 | 33 | ![Handle show results received](handle-show-results-received.svg) -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | 5 | { 6 | "name": "Vue Edge", 7 | "request": "launch", 8 | "type": "pwa-msedge", 9 | "url": "http://localhost:5000", 10 | "webRoot": "${workspaceFolder}/src", 11 | "breakOnLoad": true, 12 | "preLaunchTask": "npm run serve", 13 | "sourceMapPathOverrides": { 14 | "webpack:///src/*": "${webRoot}/*" 15 | } 16 | }, 17 | { 18 | "name": "Attach to Node Functions", 19 | "type": "node", 20 | "request": "attach", 21 | "port": 9229, 22 | "preLaunchTask": "func: host start" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.github/workflows/linkchecker.yml: -------------------------------------------------------------------------------- 1 | name: Link Checker 2 | 3 | on: 4 | schedule: 5 | - cron: "0 5 * * *" 6 | pull_request: 7 | types: [opened, edited, synchronize, reopened] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | 14 | jobs: 15 | link-checker: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: 'Checkout source code' 19 | uses: actions/checkout@v2 20 | 21 | - name: Link Checker 22 | id: lychee 23 | uses: lycheeverse/lychee-action@v1.2.0 24 | with: 25 | fail: true 26 | args: --verbose --no-progress --exclude-mail --exclude-loopback **/*.md 27 | env: 28 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SWA and Azure Functions", 3 | "runArgs": ["--init"], 4 | "build": { 5 | "dockerfile": "Dockerfile", 6 | "args": { 7 | // Update 'VARIANT' to pick a Node version: 16, 14, 12. 8 | // Append -bullseye or -buster to pin to an OS version. 9 | // Use -bullseye variants on local arm64/Apple Silicon. 10 | "VARIANT": "16" 11 | } 12 | }, 13 | "forwardPorts": [3000, 7071], 14 | "portsAttributes": { 15 | "3000": { 16 | "label": "Website", 17 | "onAutoForward": "ignore" 18 | }, 19 | "7071": { 20 | "label": "API", 21 | "onAutoForward": "ignore" 22 | } 23 | }, 24 | "settings": {}, 25 | "extensions": ["ms-azuretools.vscode-azurefunctions", "ms-azuretools.vscode-azurestaticwebapps", "dbaeumer.vscode-eslint", "vsls-contrib.codetour"], 26 | "postCreateCommand": "npm run init", 27 | "remoteUser": "node" 28 | } 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting vulnerabilities 2 | 3 | Please email reports about any security related issues you find to ably.labs@ably.com. 4 | 5 | Please use a descriptive subject line for your report email. After the initial reply to your report, the security team will endeavor to keep you informed of the progress being made towards a fix and announcement. 6 | 7 | In addition, please include the following information along with your report: 8 | 9 | - Your name and affiliation (if any). 10 | - A description of the technical details of the vulnerabilities. It is very important to let us know how we can reproduce your findings. 11 | - An explanation who can exploit this vulnerability, and what they gain when doing so -- write an attack scenario. This will help us evaluate your report quickly, especially if the issue is complex. 12 | - Whether this vulnerability public or known to third parties. If it is, please provide details. 13 | - If you believe that an existing (public) issue is security-related, include the issue ID and a short description of why it should be handled according to this security policy. 14 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | 53 | -------------------------------------------------------------------------------- /.tours/folder-structure.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "2 - Folder Structure", 4 | "steps": [ 5 | { 6 | "directory": ".github/workflows", 7 | "description": "The `.github/workflows` folder contains the GitHub workflow to deploy the application to Azure Static Web Apps.", 8 | "title": ".github/workflows" 9 | }, 10 | { 11 | "directory": ".tours", 12 | "description": "The `.tours` folder contains the CodeTour files you're now following.", 13 | "title": ".tours" 14 | }, 15 | { 16 | "directory": ".vscode", 17 | "description": "The `.vscode` folder contains files that VSCode uses to recommend extensions, apply settings and execute tasks that are relevant for this repository.", 18 | "title": ".vscode" 19 | }, 20 | { 21 | "directory": "api", 22 | "description": "The `api` folder contains the Azure Function that requests an authentication token that the Vue app will use to communicate with Ably.", 23 | "title": "api" 24 | }, 25 | { 26 | "directory": "public", 27 | "description": "The `public` folder contains the static files for the web application.", 28 | "title": "public" 29 | }, 30 | { 31 | "directory": "src", 32 | "description": "The `src` folder contains the files for the Vue application.", 33 | "title": "src" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /diagrams/start-session.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | alt client is host 3 | Client->>Vuex: startSession 4 | activate Client 5 | activate Vuex 6 | Vuex->>Vuex: generateSessionId 7 | 8 | Vuex-->>Client: sessionId 9 | else client is team member 10 | Client->>Vuex: startSession(sessionId) 11 | end 12 | Vuex->>Vuex: instantiateAblyConnection(sessionId) 13 | Vuex->>Azure Function: createTokenRequest() 14 | activate Azure Function 15 | Azure Function->>Ably: createTokenRequest() 16 | activate Ably 17 | Ably-->>Azure Function: token 18 | deactivate Ably 19 | Azure Function-->>Vuex: token 20 | deactivate Azure Function 21 | Vuex->>Ably: new RealtimeInstance(token) 22 | activate Ably 23 | Ably-->>Vuex: instance 24 | Vuex->>Vuex: setAblyClientId 25 | Vuex->>Ably: attachToAblyChannels(channelName) 26 | Ably-->>Vuex: channel 27 | Vuex->>Ably: subscribeToAblyVoting() 28 | Vuex->>Ably: getExistingAblyPresenceSet() 29 | Ably-->>Vuex: participants 30 | loop for each participant 31 | Vuex->>Vuex: addParticipantJoined 32 | end 33 | Vuex->>Ably: subscribeToAblyPresence() 34 | Vuex->>Ably: enterClientInAblyPresenceSet() 35 | Client->>Vuex: numberOfParticipantsJoined() 36 | Vuex-->>Client: count 37 | deactivate Vuex 38 | Ably->>Other Clients: broadcast: participant entered 39 | deactivate Ably 40 | deactivate Client 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agile-flush", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "init": "npm clean-install && cd api && npm clean-install", 7 | "dev": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview", 10 | "func": "cd api && func start", 11 | "swa": "swa start http://localhost:5173 --api-location http://localhost:7071", 12 | "all": "npm-run-all --parallel dev func swa", 13 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/ably-labs/agile-flush-vue-app" 18 | }, 19 | "author": "Marc Duiker", 20 | "license": "Apache-2.0", 21 | "bugs": { 22 | "url": "https://github.com/ably-labs/agile-flush-vue-app/issues" 23 | }, 24 | "dependencies": { 25 | "ably": "^1.2.15", 26 | "vue": "^3.2.26", 27 | "vue-router": "^4.0.12", 28 | "vuex": "^4.0.2" 29 | }, 30 | "devDependencies": { 31 | "@azure/static-web-apps-cli": "^1.0.3", 32 | "@vitejs/plugin-vue": "^3.2.0", 33 | "eslint": "^8.26.0", 34 | "eslint-config-prettier": "^8.5.0", 35 | "eslint-config-standard": "^17.0.0", 36 | "eslint-plugin-import": "^2.25.4", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-prettier": "^4.2.1", 39 | "eslint-plugin-promise": "^6.1.1", 40 | "eslint-plugin-vue": "^8.2.0", 41 | "npm-run-all": "^4.1.5", 42 | "prettier": "^2.5.1", 43 | "vite": "^3.2.2" 44 | }, 45 | "eslintConfig": { 46 | "root": true, 47 | "env": { 48 | "node": true 49 | }, 50 | "extends": [ 51 | "plugin:vue/essential", 52 | "eslint:recommended" 53 | ], 54 | "rules": {} 55 | }, 56 | "browserslist": [ 57 | "> 1%", 58 | "last 2 versions" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | import { generateSessionId } from '../util/sessionIdGenerator'; 3 | import { cardModule } from './cardModule'; 4 | import { realtimeModule } from './realtimeModule'; 5 | 6 | export const store = createStore({ 7 | modules: { 8 | cards: cardModule, 9 | realtime: realtimeModule, 10 | }, 11 | state: { 12 | sessionId: null, 13 | showResults: false, 14 | }, 15 | getters: { 16 | sessionId: (state) => state.sessionId, 17 | hasSessionStarted: (state) => state.sessionId !== null && state.sessionId !== undefined, 18 | showResults: (state) => state.showResults, 19 | }, 20 | mutations: { 21 | setSessionId(state, sessionId) { 22 | state.sessionId = sessionId; 23 | }, 24 | setShowResults(state, showResults) { 25 | state.showResults = showResults; 26 | }, 27 | toggleShowResults(state) { 28 | state.showResults = !state.showResults; 29 | }, 30 | }, 31 | actions: { 32 | commonResetVoting({ commit }) { 33 | const flush = new Audio('flush.mp3'); 34 | flush.play(); 35 | commit('resetCards'); 36 | commit('setShowResults', false); 37 | }, 38 | resetVoting({ dispatch }) { 39 | dispatch('commonResetVoting'); 40 | dispatch('publishResetVotingToAbly'); 41 | }, 42 | startSession({ commit }, routeSessionId) { 43 | let sessionId; 44 | if (routeSessionId == null) { 45 | sessionId = generateSessionId(); 46 | } else { 47 | sessionId = routeSessionId; 48 | } 49 | commit('setSessionId', sessionId); 50 | }, 51 | toggleShowResults({ dispatch, commit, getters }) { 52 | commit('toggleShowResults'); 53 | dispatch( 54 | 'publishShowResultsToAbly', 55 | getters.showResults, 56 | ); 57 | }, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json 95 | 96 | # Azurite artifacts 97 | __blobstorage__ 98 | __queuestorage__ 99 | __azurite_db*__.json -------------------------------------------------------------------------------- /src/components/SessionSection.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 64 | 65 | 85 | -------------------------------------------------------------------------------- /src/components/HomePage.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 63 | 64 | 80 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-gentle-moss-08d9e3303.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**/*.md' 9 | - 'diagrams/*.*' 10 | pull_request: 11 | types: [opened, synchronize, reopened, closed] 12 | branches: 13 | - main 14 | paths-ignore: 15 | - '**/*.md' 16 | - 'diagrams/*.*' 17 | 18 | jobs: 19 | build_and_deploy_job: 20 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 21 | runs-on: ubuntu-latest 22 | name: Build and Deploy Job 23 | steps: 24 | - uses: actions/checkout@v2 25 | with: 26 | submodules: true 27 | - name: Build And Deploy 28 | id: builddeploy 29 | uses: Azure/static-web-apps-deploy@v1 30 | with: 31 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GENTLE_MOSS_08D9E3303 }} 32 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 33 | action: "upload" 34 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 35 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 36 | app_location: "/" # App source code path 37 | api_location: "api" # Api source code path - optional 38 | output_location: "dist" # Built app content directory - optional 39 | ###### End of Repository/Build Configurations ###### 40 | 41 | close_pull_request_job: 42 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 43 | runs-on: ubuntu-latest 44 | name: Close Pull Request Job 45 | steps: 46 | - name: Close Pull Request 47 | id: closepullrequest 48 | uses: Azure/static-web-apps-deploy@v1 49 | with: 50 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GENTLE_MOSS_08D9E3303 }} 51 | action: "close" 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | # OS 121 | .DS_Store -------------------------------------------------------------------------------- /.tours/azure-functions-app.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "3 - Azure Functions App", 4 | "steps": [ 5 | { 6 | "directory": "api", 7 | "description": "The `api` folder contains the files for the Azure Functions App.", 8 | "title": "api" 9 | }, 10 | { 11 | "directory": "api/createTokenRequest", 12 | "description": "The `createTokenRequest` folder contains the function definition of the Node based Azure Function.", 13 | "title": "createTokenRequest" 14 | }, 15 | { 16 | "file": "api/createTokenRequest/function.json", 17 | "description": "The `function.json` contains the function binding information that describes that this function is triggered by an HTTP GET request.", 18 | "line": 10, 19 | "selection": { 20 | "start": { 21 | "line": 4, 22 | "character": 7 23 | }, 24 | "end": { 25 | "line": 10, 26 | "character": 8 27 | } 28 | }, 29 | "title": "function.json" 30 | }, 31 | { 32 | "file": "api/createTokenRequest/index.js", 33 | "description": "The `index.js` file contains the function that will be executed, and contains references to any Node modules that it requires.", 34 | "line": 1, 35 | "title": "index.js" 36 | }, 37 | { 38 | "file": "api/createTokenRequest/index.js", 39 | "description": "The function constructs a new Ably client and requires an API key (`ABLY_API_KEY`) that is set in the configuration of the Azure Static Web App (or locally in the `local.settings.json`).", 40 | "line": 5, 41 | "title": "index.js - ably client" 42 | }, 43 | { 44 | "file": "api/createTokenRequest/index.js", 45 | "description": "An authentication token is requested from the Ably client and that token is returned in the HTTP response.", 46 | "line": 10, 47 | "selection": { 48 | "start": { 49 | "line": 6, 50 | "character": 5 51 | }, 52 | "end": { 53 | "line": 10, 54 | "character": 7 55 | } 56 | }, 57 | "title": "index.js - create token" 58 | }, 59 | { 60 | "file": "api/host.json", 61 | "description": "The `hosts.json` file contains global settings regarding logging and references which binding extensions are used. More info on the `hosts.json` file is found in the [official Azure docs](https://docs.microsoft.com/en-us/azure/azure-functions/functions-host-json).", 62 | "line": 15, 63 | "selection": { 64 | "start": { 65 | "line": 11, 66 | "character": 4 67 | }, 68 | "end": { 69 | "line": 11, 70 | "character": 19 71 | } 72 | }, 73 | "title": "host.json" 74 | }, 75 | { 76 | "directory": "api", 77 | "description": "If you want to run the application locally you need to add a file named `local.settings.json` in the `api` folder. This file is ignored by git because it may contain secrets like API keys.\n\nThe content should look like this:\n\n```json\n{\n \"IsEncrypted\": false,\n \"Values\": {\n \"AzureWebJobsStorage\": \"\",\n \"FUNCTIONS_WORKER_RUNTIME\": \"node\",\n \"ABLY_API_KEY\": \"\"\n }\n}\n```\n\nWhere `` should be replaced with [an actual API key](https://faqs.ably.com/setting-up-and-managing-api-keys).", 78 | "title": "local.settings.json" 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /src/store/cardModule.js: -------------------------------------------------------------------------------- 1 | export const cardModule = { 2 | state: () => ({ 3 | cards: [ 4 | { 5 | number: '0', 6 | count: [], 7 | style: 'card0', 8 | }, 9 | { 10 | number: '0.5', 11 | count: [], 12 | style: 'card05', 13 | }, 14 | { 15 | number: '1', 16 | count: [], 17 | style: 'card1', 18 | }, 19 | { 20 | number: '2', 21 | count: [], 22 | style: 'card2', 23 | }, 24 | { 25 | number: '3', 26 | count: [], 27 | style: 'card3', 28 | }, 29 | { 30 | number: '5', 31 | count: [], 32 | style: 'card5', 33 | }, 34 | { 35 | number: '8', 36 | count: [], 37 | style: 'card8', 38 | }, 39 | { 40 | number: '13', 41 | count: [], 42 | style: 'card13', 43 | }, 44 | { 45 | number: '21', 46 | count: [], 47 | style: 'card21', 48 | }, 49 | ], 50 | }), 51 | getters: { 52 | cards: (state) => state.cards, 53 | cardIndex: (state) => (cardNumber) => state.cards.findIndex((card) => card.number === cardNumber), 54 | voteCountForCard: (state) => (cardNumber) => state.cards.filter((card) => card.number === cardNumber)[0].count 55 | .length, 56 | isCardSelectedByClient: (state, getters) => (cardNumber) => { 57 | const clientIds = state.cards.filter((card) => card.number === cardNumber)[0].count; 58 | if (clientIds.length > 0) { 59 | return clientIds.includes(getters.clientId); 60 | } 61 | return false; 62 | }, 63 | isAnyCardSelectedByClient: (state, getters) => { 64 | const cardCount = state.cards.filter( 65 | (card) => card.count.length > 0 && card.count.includes(getters.clientId), 66 | ).length; 67 | return cardCount > 0; 68 | }, 69 | selectedCardForClient: (state) => (clientId) => { 70 | const selectedByClient = state.cards.filter((card) => card.count.length > 0 71 | && card.count.includes(clientId))[0]; 72 | if (selectedByClient !== undefined) { 73 | return selectedByClient.number; 74 | } 75 | return null; 76 | }, 77 | numberOfParticipantsVoted: (state) => { 78 | const concatenatedCount = []; 79 | state.cards.forEach((card) => { 80 | concatenatedCount.push(...card.count); 81 | }); 82 | return concatenatedCount.length; 83 | }, 84 | }, 85 | mutations: { 86 | addParticipantVoted(state, clientVote) { 87 | const index = this.getters.cardIndex(clientVote.cardNumber); 88 | if (!state.cards[index].count.includes(clientVote.clientId)) { 89 | state.cards[index].count.push(clientVote.clientId); 90 | } 91 | }, 92 | removeParticipantVoted(state, clientVote) { 93 | const index = this.getters.cardIndex(clientVote.cardNumber); 94 | if (state.cards[index].count.includes(clientVote.clientId)) { 95 | state.cards[index].count.splice( 96 | state.cards[index].count.findIndex( 97 | (id) => id === clientVote.clientId, 98 | ), 99 | 1, 100 | ); 101 | } 102 | }, 103 | resetCards(state) { 104 | state.cards.forEach((card) => { 105 | card.count = []; 106 | }); 107 | }, 108 | }, 109 | actions: { 110 | doVote({ dispatch, commit, getters }, cardNumber) { 111 | const clientVote = { 112 | clientId: getters.clientId, 113 | cardNumber, 114 | }; 115 | commit('addParticipantVoted', clientVote); 116 | dispatch('publishVoteToAbly', clientVote); 117 | }, 118 | undoVote({ dispatch, commit, getters }, cardNumber) { 119 | const clientVote = { 120 | clientId: getters.clientId, 121 | cardNumber, 122 | }; 123 | commit('removeParticipantVoted', clientVote); 124 | dispatch('publishUndoVoteToAbly', clientVote); 125 | }, 126 | }, 127 | }; 128 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to GitHub docs contributing guide 2 | 3 | Thank you for investing your time in contributing to our project! :sparkles: 4 | 5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | ## New contributor guide 10 | 11 | To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions: 12 | 13 | - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) 14 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 15 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 16 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 17 | 18 | ## Getting started 19 | 20 | If you would like to add a new feature please submit an issue first that describes the feature in detail. Once there is agreement on the new feature you can continue and submit a pull request. 21 | 22 | ### Issues 23 | 24 | #### Create a new issue 25 | 26 | If you spot a problem, [search if an issue already exists](https://github.com/ably-labs/planning-poker-vue-app/issues). If a related issue doesn't exist, you can open a [new issue](https://github.com/ably-labs/planning-poker-vue-app/issues/new). 27 | 28 | #### Solve an issue 29 | 30 | Scan through our [existing issues](https://github.com/ably-labs/planning-poker-vue-app/issues) to find one that interests you. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix. 31 | 32 | #### Make changes locally 33 | 34 | 1. [Install Git](https://docs.github.com/en/get-started/quickstart/set-up-git#setting-up-git). 35 | 36 | 2. Fork the repository. 37 | 38 | - Using GitHub Desktop: 39 | - [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop. 40 | - Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)! 41 | 42 | - Using the command line: 43 | - [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them. 44 | 45 | - GitHub Codespaces: 46 | - [Fork, edit, and preview](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace) using [GitHub Codespaces](https://github.com/features/codespaces) without having to install and run the project locally. 47 | 48 | 3. Create a working branch and start with your changes! 49 | 50 | ### Commit your update 51 | 52 | Commit the changes once you are happy with them. See [Atom's contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages) to know how to use emoji for commit messages. 53 | 54 | ### Pull Request 55 | 56 | When you're finished with the changes, create a pull request, also known as a PR. 57 | 58 | - Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request. 59 | - Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one. 60 | - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. 61 | Once you submit your PR, a Docs team member will review your proposal. We may ask questions or request for additional information. 62 | - We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. 63 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). 64 | - If you run into any merge issues, checkout this [git tutorial](https://lab.github.com/githubtraining/managing-merge-conflicts) to help you resolve merge conflicts and other issues. 65 | 66 | ### Your PR is merged! 67 | 68 | Congratulations :tada::tada: The Ably team thanks you! :sparkles: -------------------------------------------------------------------------------- /src/components/CardItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 68 | 69 | 130 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | ably.labs@ably.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /src/store/realtimeModule.js: -------------------------------------------------------------------------------- 1 | import { Realtime } from 'ably/promises'; 2 | 3 | export const realtimeModule = { 4 | state: () => ({ 5 | ablyClientId: null, 6 | ablyRealtimeClient: null, 7 | channelNames: { 8 | voting: 'voting', 9 | }, 10 | channelInstances: { 11 | voting: null, 12 | }, 13 | isAblyConnected: false, 14 | participantsJoinedArr: [], 15 | }), 16 | getters: { 17 | clientId: (state) => state.ablyClientId, 18 | haveParticipantsJoined: (state) => state.participantsJoinedArr.length > 1, 19 | isAblyConnected: (state) => state.isAblyConnected, 20 | numberOfParticipantsJoined: (state) => state.participantsJoinedArr.length, 21 | }, 22 | mutations: { 23 | addParticipantJoined(state, clientId) { 24 | if (!state.participantsJoinedArr.includes(clientId)) { 25 | state.participantsJoinedArr.push(clientId); 26 | } 27 | }, 28 | removeParticipantJoined(state, clientId) { 29 | state.participantsJoinedArr.splice( 30 | state.participantsJoinedArr.findIndex( 31 | (participant) => participant.id === clientId, 32 | ), 33 | 1, 34 | ); 35 | }, 36 | setAblyChannelInstances(state, { voting }) { 37 | state.channelInstances.voting = voting; 38 | }, 39 | setAblyClientId(state, clientId) { 40 | state.ablyClientId = clientId; 41 | }, 42 | setAblyConnectionStatus(state, status) { 43 | state.isAblyConnected = status; 44 | }, 45 | setAblyRealtimeClient(state, ablyRealtimeClient) { 46 | state.ablyRealtimeClient = ablyRealtimeClient; 47 | }, 48 | }, 49 | actions: { 50 | instantiateAblyConnection({ 51 | dispatch, commit, state, getters, 52 | }, ids) { 53 | if (!getters.isAblyConnected) { 54 | const realtimeClient = new Realtime({ 55 | authUrl: '/api/createTokenRequest', 56 | echoMessages: false, 57 | }); 58 | realtimeClient.connection.on('connected', () => { 59 | commit('setAblyConnectionStatus', true); 60 | commit('setAblyRealtimeClient', realtimeClient); 61 | commit( 62 | 'setAblyClientId', 63 | ids.clientId ?? state.ablyRealtimeClient.auth.clientId, 64 | ); 65 | if (ids.sessionId) { 66 | commit('setSessionId', ids.sessionId); 67 | } 68 | dispatch('attachToAblyChannels').then(() => { 69 | dispatch('enterClientInAblyPresenceSet'); 70 | dispatch('getExistingAblyPresenceSet').then(() => { 71 | dispatch('subscribeToAblyPresence'); 72 | }); 73 | }); 74 | }); 75 | 76 | realtimeClient.connection.on('disconnected', () => { 77 | commit('setAblyConnectionStatus', false); 78 | }); 79 | } 80 | }, 81 | closeAblyConnection({ state }) { 82 | state.ablyRealtimeClient.connection.close(); 83 | }, 84 | async attachToAblyChannels({ 85 | dispatch, commit, getters, state, 86 | }) { 87 | const channelName = `${state.channelNames.voting}-${getters.sessionId}`; 88 | const votingChannel = await state.ablyRealtimeClient.channels.get( 89 | channelName, 90 | { 91 | params: { rewind: '2m' }, 92 | }, 93 | ); 94 | commit('setAblyChannelInstances', { 95 | voting: votingChannel, 96 | }); 97 | dispatch('subscribeToAblyVoting'); 98 | }, 99 | enterClientInAblyPresenceSet({ state }) { 100 | state.channelInstances.voting.presence.enter({ 101 | id: state.ablyClientId, 102 | }); 103 | }, 104 | async getExistingAblyPresenceSet({ commit, state }) { 105 | await state.channelInstances.voting.presence.get((err, participants) => { 106 | if (!err) { 107 | for (let i = 0; i < participants.length; i++) { 108 | commit('addParticipantJoined', participants[i].clientId); 109 | } 110 | } 111 | }); 112 | }, 113 | subscribeToAblyPresence({ dispatch, state }) { 114 | state.channelInstances.voting.presence.subscribe('enter', (msg) => { 115 | dispatch('handleNewParticipantEntered', msg); 116 | }); 117 | state.channelInstances.voting.presence.subscribe('leave', (msg) => { 118 | dispatch('handleExistingParticipantLeft', msg); 119 | }); 120 | }, 121 | 122 | handleNewParticipantEntered({ commit }, participant) { 123 | commit('addParticipantJoined', participant.clientId); 124 | }, 125 | handleExistingParticipantLeft({ commit, getters }, participant) { 126 | commit('removeParticipantJoined', participant.clientId); 127 | const cardNumber = getters.selectedCardForClient(participant.clientId); 128 | if (cardNumber !== null) { 129 | commit('removeParticipantVoted', { 130 | clientId: participant.clientId, 131 | cardNumber, 132 | }); 133 | } 134 | }, 135 | 136 | subscribeToAblyVoting({ dispatch, state }) { 137 | state.channelInstances.voting.subscribe('vote', (msg) => { 138 | dispatch('handleVoteReceived', msg); 139 | }); 140 | state.channelInstances.voting.subscribe('undo-vote', (msg) => { 141 | dispatch('handleUndoVoteReceived', msg); 142 | }); 143 | state.channelInstances.voting.subscribe('show-results', (msg) => { 144 | dispatch('handleShowResultsReceived', msg); 145 | }); 146 | state.channelInstances.voting.subscribe('reset-voting', (msg) => { 147 | dispatch('handleResetVotingReceived', msg); 148 | }); 149 | }, 150 | 151 | handleVoteReceived({ commit }, msg) { 152 | commit('addParticipantVoted', { 153 | clientId: msg.data.clientId, 154 | cardNumber: msg.data.cardNumber, 155 | }); 156 | }, 157 | handleUndoVoteReceived({ commit }, msg) { 158 | commit('removeParticipantVoted', { 159 | clientId: msg.data.clientId, 160 | cardNumber: msg.data.cardNumber, 161 | }); 162 | }, 163 | handleShowResultsReceived({ commit }, msg) { 164 | if (msg.data.showResults) { 165 | commit('setShowResults', true); 166 | } else { 167 | commit('setShowResults', false); 168 | } 169 | }, 170 | // eslint-disable-next-line no-unused-vars 171 | handleResetVotingReceived({ dispatch }, msg) { 172 | dispatch('commonResetVoting'); 173 | }, 174 | 175 | publishVoteToAbly({ state }, clientVote) { 176 | state.channelInstances.voting.publish('vote', clientVote); 177 | }, 178 | publishUndoVoteToAbly({ state }, clientVote) { 179 | state.channelInstances.voting.publish('undo-vote', clientVote); 180 | }, 181 | publishShowResultsToAbly({ state }, showResults) { 182 | state.channelInstances.voting.publish('show-results', { 183 | showResults, 184 | }); 185 | }, 186 | publishResetVotingToAbly({ state }) { 187 | state.channelInstances.voting.publish('reset-voting', {}); 188 | }, 189 | }, 190 | }; 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ♥♣🚽 Agile Flush 🚽♠♦ 2 | 3 | The No. 1 and No. 2 place for online planning poker! Watch [this video](https://youtu.be/59BZCQuRRkM) for more details. 4 | 5 | [![AgileFlush Screenshot](agileflush_screenshot.png)](https://agileflush.ably.dev/) 6 | 7 | This project is an example of how an online collaboration tool can be built that depends on realtime data synchronization between clients. 8 | 9 | This is the flow diagram how users can interact with the application. 10 | 11 | ![Functionality diagram](/diagrams/agile-flush-functionality.png) 12 | 13 | Once a planning session has started, all user actions are synchronized across all team members to maintain a consistent state. So *Show/hide votes* and *Reset votes* are triggered for all connected users once these buttons are clicked. 14 | 15 | The live version can be used here: [Agile Flush on Azure](https://agileflush.ably.dev/). 16 | 17 | ## The tech stack 18 | 19 | ![Component diagram](/diagrams/aglile-flush-main-technical-components.png) 20 | 21 | The project uses the following components: 22 | 23 | - [Vue 3](https://v3.vuejs.org/) as the front-end framework. 24 | - [NodeJS Azure Function](https://docs.microsoft.com/azure/developer/javascript/how-to/develop-serverless-apps) to do the authentication with Ably. 25 | - [Ably](https://ably.com?utm_campaign=GLB-2201-agile-flush&utm_content=repo-agile-flush&utm_source=github&utm_medium=repo&src=GLB-2201-agile-flush-github) as the realtime communications platform. 26 | - [Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/overview) to host the Vue application and NodeJS function. 27 | 28 | ## Building & running the app yourself 29 | 30 | There are two options: 31 | 32 | 1. [Install & run locally](#1-install--run-locally) 33 | 1. [Run using GitHub Codespaces](#2-run-using-github-codespaces) 34 | 35 | ### 1. Install & run locally 36 | 37 | #### Prerequisites 38 | 39 | Ensure you have the following dependencies installed: 40 | 41 | - [Node 16](https://nodejs.org/en/download/) 42 | - [Azure Static Web Apps CLI](https://github.com/Azure/static-web-apps-cli) 43 | - [Azure Functions Core Tools v4](https://docs.microsoft.com/azure/azure-functions/functions-run-local?tabs=v4) 44 | 45 | For more info developing Static Web Apps locally see the [official Azure docs](https://docs.microsoft.com/azure/static-web-apps/local-development). 46 | 47 | #### Installation steps 48 | 49 | 1. Clone this repository to your local machine. 50 | 51 | 📝 **Tip** - If you intend you deploy your own version of the app, you're better off forking this repo. 52 | 53 | 1. To install the dependencies for this application, run this in the root of the repository: 54 | 55 | ```cmd 56 | npm run init 57 | ``` 58 | 59 | 1. Now continue with [Running the application](#running-the-application). 60 | 61 | ### 2. Run using GitHub Codespaces 62 | 63 | 1. If you're new to Codespaces, please have a look at the [quickstart in the GitHub docs](https://docs.github.com/en/codespaces/getting-started/quickstart). 64 | 65 | 1. Create a new Codespace via the *<> Code* dropdown button and select the tab *Codespaces -> New codespace*. 66 | 67 | 1. Now continue with [Running the application](#running-the-application). 68 | 69 | ### Running the application 70 | 71 | 1. [Sign up](https://ably.com/signup?utm_campaign=GLB-2201-agile-flush&utm_content=repo-agile-flush&utm_source=github&utm_medium=repo&src=GLB-2201-agile-flush-github) or [log in](https://ably.com/login?utm_campaign=GLB-2201-agile-flush&utm_content=repo-agile-flush&utm_source=github&utm_medium=repo&src=GLB-2201-agile-flush-github) to ably.com, and [create a new app and copy the API key](https://faqs.ably.com/setting-up-and-managing-api-keys?utm_campaign=GLB-2201-agile-flush&utm_content=repo-agile-flush&utm_source=github&utm_medium=repo&src=GLB-2201-agile-flush-github). 72 | 73 | 1. Add a file named `local.settings.json` to the `api` folder and add the following content: 74 | 75 | ```json 76 | { 77 | "IsEncrypted": false, 78 | "Values": { 79 | "AzureWebJobsStorage": "", 80 | "FUNCTIONS_WORKER_RUNTIME": "node", 81 | "ABLY_API_KEY": "" 82 | } 83 | } 84 | ``` 85 | 86 | - Replace `` with the key you copied in the previous step. 87 | 88 | 1. To run everything (Vue application, Azure Function, and Static Web Apps emulator), run this in the root of the repository: 89 | 90 | ```cmd 91 | swa start 92 | ``` 93 | 94 | Navigate to `http://localhost:4280` to run the application locally. 95 | 96 |
97 | If these ports are already in use, please change them in the `package.json` file or start the apps individually. 98 | 99 | A. To run the Vue application, run this in the root of the repository: 100 | 101 | ```cmd 102 | npm run dev 103 | ``` 104 | 105 | The Vue app will be available at `http://localhost:5173`. 106 | 107 | B. To start the Azure Functions runtime, run this in the `api` folder of the repository: 108 | 109 | ```cmd 110 | func start 111 | ``` 112 | 113 | The Azure Functions app will be available at `http://localhost:7071`. 114 | 115 | C. To start the Static Web App emulator, run this in the root of the repository: 116 | 117 | ```cmd 118 | swa start http://localhost:5173 --api-location http://localhost:7071 119 | ``` 120 | 121 |
122 | 123 | 1. Now you can use the application using the endpoint provided by the Static Web Apps emulator: `http://localhost:4280`. 124 | 125 | ## Deploying the app to Azure 126 | 127 | Once you forked this repository, you can deploy it to Azure via: 128 | 129 | - [the Azure Portal](https://docs.microsoft.com/azure/static-web-apps/get-started-portal?tabs=vue) or 130 | - [the Azure CLI](https://docs.microsoft.com/azure/static-web-apps/get-started-cli?tabs=vue) 131 | 132 | In both cases you can skip the repository creation step since you can use your fork of this repository. 133 | 134 | ## Code Tours 135 | 136 | This repository has code tours that guide you through the files and folders in this repository. You can either start the tours in VSCode (enabled by the [CodeTour extension](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour)) or you start them by visiting the [github.dev](https://github.dev/ably-labs/agile-flush-vue-app) version of this repository. 137 | 138 | ## More info 139 | 140 | Want more information about this sample or using Ably in general? Feel free to contact me on [Discord](http://go.ably.com/discord). 141 | 142 | - [Join our Discord server](http://go.ably.com/discord) 143 | - [Follow us on Twitter](https://twitter.com/ablyrealtime) 144 | - [Visit our website](https://ably.com?utm_campaign=GLB-2201-agile-flush&utm_content=repo-agile-flush&utm_source=github&utm_medium=repo&src=GLB-2201-agile-flush-github) 145 | 146 | --- 147 | [![Ably logo](https://static.ably.dev/badge-black.svg?agile-flush-vue-app)](https://ably.com?utm_campaign=GLB-2201-agile-flush&utm_content=repo-agile-flush&utm_source=github&utm_medium=repo&src=GLB-2201-agile-flush-github) 148 | -------------------------------------------------------------------------------- /diagrams/handle-show-results-received.svg: -------------------------------------------------------------------------------- 1 | AblyVuexClientbroadcast: show-resultshandleShowResultsReceived(bool)setShowResults(bool)showResults()boolAblyVuexClient -------------------------------------------------------------------------------- /diagrams/handle-newparticipant-entered.svg: -------------------------------------------------------------------------------- 1 | AblyVuexClientbroadcast: participant enteredhandleNewParticipantEntered(participant)addParticipantJoined(clientId)numberOfParticipantsJoined()countAblyVuexClient -------------------------------------------------------------------------------- /diagrams/handle-vote-received.svg: -------------------------------------------------------------------------------- 1 | AblyVuexClientbroadcast: votehandleVoteReceived(vote)addParticipantVoted(vote)voteCountForCard(cardNumber)countnumberOfParticipantsVoted()countAblyVuexClient -------------------------------------------------------------------------------- /diagrams/handle-reset-voting-received.svg: -------------------------------------------------------------------------------- 1 | AblyVuexClientbroadcast: reset-votinghandleResetVotingReceived(vote)commonResetVoting(vote)voteCountForCard(cardNumber)countnumberOfParticipantsVoted()countAblyVuexClient -------------------------------------------------------------------------------- /diagrams/toggle-show-results.svg: -------------------------------------------------------------------------------- 1 | ClientVuexAblyOther ClientstoggleShowResults()toggleShowResults()publish(show-results)broadcast: show-resultsshowResults()boolClientVuexAblyOther Clients -------------------------------------------------------------------------------- /diagrams/reset-votes.svg: -------------------------------------------------------------------------------- 1 | ClientVuexAblyOther ClientsresetVoting()commonResetVoting()publish(reset-voting)broadcast: reset-votingvoteCountForCard(cardNumber)countnumberOfParticipantsVoted()countClientVuexAblyOther Clients -------------------------------------------------------------------------------- /diagrams/components.svg: -------------------------------------------------------------------------------- 1 |
Azure Static Web Apps
Ably
channel
Node.js Azure Function
Vue.js application
Ably app
-------------------------------------------------------------------------------- /diagrams/do-voting.svg: -------------------------------------------------------------------------------- 1 | ClientVuexAblyOther ClientsselectCard(cardNumber)doVote(cardNumber)addParticipantVoted(vote)publish(vote)broadcast: votevoteCountForCard(cardNumber)countnumberOfParticipantsVoted()countClientVuexAblyOther Clients -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /.tours/vue-app.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "4 - Vue App", 4 | "steps": [ 5 | { 6 | "directory": "src", 7 | "description": "The `src` folder contains the files for the Vue App.", 8 | "title": "src" 9 | }, 10 | { 11 | "file": "src/main.js", 12 | "description": "The `main.js` file is the entry point for the app that creates a new Vue instance. Two Vue plugins are used: [Vue Router](https://router.vuejs.org/) for the routing and [Vuex](https://vuex.vuejs.org/) for state management.", 13 | "line": 12, 14 | "selection": { 15 | "start": { 16 | "line": 6, 17 | "character": 1 18 | }, 19 | "end": { 20 | "line": 9, 21 | "character": 47 22 | } 23 | }, 24 | "title": "main.js" 25 | }, 26 | { 27 | "file": "src/App.vue", 28 | "description": "The `App.vue` file is the root component of the application. It references `HomePage` component and Vuex getters and actions.", 29 | "line": 3, 30 | "title": "App.vue" 31 | }, 32 | { 33 | "file": "src/App.vue", 34 | "description": "The `beforeMount` lifecycle event is used to detect if a sessionId is present in the route. This would be the case when the first participant starts a session and shares the link with other participants. When the sessionId is present, a new Ably connection will be created for this participant.", 35 | "line": 24, 36 | "selection": { 37 | "start": { 38 | "line": 39, 39 | "character": 3 40 | }, 41 | "end": { 42 | "line": 41, 43 | "character": 4 44 | } 45 | }, 46 | "title": "App.vue - beforeMount" 47 | }, 48 | { 49 | "file": "src/App.vue", 50 | "description": "Once the Vue app is unmounted, the connection with Ably is terminated.", 51 | "line": 35, 52 | "title": "App.vue - unmounted" 53 | }, 54 | { 55 | "file": "src/components/HomePage.vue", 56 | "description": "The `HomePage` contains the high level page layout.", 57 | "line": 1, 58 | "title": "HomePage.vue" 59 | }, 60 | { 61 | "file": "src/components/HomePage.vue", 62 | "description": "This component references two other components; `SessionSection`, and `FooterSection`, as well as several Vuex getters and actions.", 63 | "line": 35, 64 | "selection": { 65 | "start": { 66 | "line": 44, 67 | "character": 1 68 | }, 69 | "end": { 70 | "line": 48, 71 | "character": 47 72 | } 73 | }, 74 | "title": "HomePage.vue - imports" 75 | }, 76 | { 77 | "file": "src/components/HomePage.vue", 78 | "description": "The `start()` method is called when the first participant clicks the *Start Session* button. A new connection to Ably is made, and the client is subscribed to the channel. To allow the other participants to join this session, the sessionId is added to the url, and the url is copied to the clipboard so it can be shared easily.", 79 | "line": 50, 80 | "title": "HomePage.vue - start" 81 | }, 82 | { 83 | "file": "src/components/SessionSection.vue", 84 | "description": "The `SessionSection` component contains the template for once a session has started.", 85 | "line": 2, 86 | "title": "SessionSection.vue" 87 | }, 88 | { 89 | "file": "src/components/SessionSection.vue", 90 | "description": "The template uses Vuex getters to retrieve the number of participants that have joined & voted.", 91 | "line": 9, 92 | "title": "SessionSection - participants" 93 | }, 94 | { 95 | "file": "src/components/SessionSection.vue", 96 | "description": "There is a button to toggle the visiblity of the voting results. This button triggers a Vuex action that publishes a `show-results` message to the other clients.", 97 | "line": 15, 98 | "title": "SessionSection - toggle results" 99 | }, 100 | { 101 | "file": "src/components/SessionSection.vue", 102 | "description": "The _Flush votes_ button resets the votes and uses a Vuex action to publish a `reset-votes` message to the clients.", 103 | "line": 19, 104 | "title": "SessionSection - flush votes" 105 | }, 106 | { 107 | "file": "src/components/SessionSection.vue", 108 | "description": "For each of the cards in the `cards` collection the `CardItem` component is used.", 109 | "line": 31, 110 | "title": "SessionSection - cards collection" 111 | }, 112 | { 113 | "file": "src/components/CardItem.vue", 114 | "description": "The `CardItem` component is used to render the individual voting cards.", 115 | "line": 1, 116 | "title": "CardItem.vue" 117 | }, 118 | { 119 | "file": "src/components/CardItem.vue", 120 | "description": "The `selectCard()` function is called when a participant clicks on a card. Since only one card can be selected at any time a check is made before a card is selected. If no card is selected, the `doVote()` function is called which sends a *vote* message to all connected clients. If the selected card is selected again, the `undoVote()` function is called, which deselects the card and sends an *undo-vote* mnessage to all connected clients.", 121 | "line": 49, 122 | "selection": { 123 | "start": { 124 | "line": 42, 125 | "character": 5 126 | }, 127 | "end": { 128 | "line": 60, 129 | "character": 5 130 | } 131 | }, 132 | "title": "CardItem - selectCard" 133 | }, 134 | { 135 | "file": "src/components/CardItem.vue", 136 | "description": "The visual representation of the voting cards are present as ASCII art in CSS.", 137 | "line": 95, 138 | "title": "CardItem - CSS card content" 139 | }, 140 | { 141 | "file": "src/components/FooterSection.vue", 142 | "description": "The `FooterSection` component renders this small part of static html with contact details.", 143 | "line": 1, 144 | "title": "FooterSection.vue" 145 | }, 146 | { 147 | "file": "src/router/index.js", 148 | "description": "The `router/index.js` script contains [Vue Router](https://next.router.vuejs.org/) configuration.", 149 | "line": 1, 150 | "title": "Vue Router" 151 | }, 152 | { 153 | "file": "src/store/index.js", 154 | "description": "The `store/index.js` script contains the [Vuex](https://next.vuex.vuejs.org/) configuration for state management.", 155 | "line": 1, 156 | "selection": { 157 | "start": { 158 | "line": 362, 159 | "character": 5 160 | }, 161 | "end": { 162 | "line": 362, 163 | "character": 26 164 | } 165 | }, 166 | "title": "Vuex store" 167 | }, 168 | { 169 | "file": "src/store/index.js", 170 | "description": "The Vuex store uses seperate modules to prevent having everything in one big file. \n- The `cardModule` contains the state and behaviour for anything related to the voting cards.\n- The `realtimeModule` contains the state and behaviour for anything related to Ably Realtime, for instance; the connection, the channels, and publishing messages.", 171 | "line": 5, 172 | "title": "Vuex store - modules" 173 | }, 174 | { 175 | "file": "src/store/cardModule.js", 176 | "description": "The `state` tree contains all the data fields we want to update and use in the components.", 177 | "line": 11, 178 | "title": "store - state" 179 | }, 180 | { 181 | "file": "src/store/cardModule.js", 182 | "description": "The `cards` property is one of the most important ones. It contains definitions for all of the voting cards (numbers and ASCII visuals) as well as a `count` array that is used to store the clientIds of the participants who selected that card.\n\nIf you want more cards, like a coffee card, or an infinity card, you can add these to this `cards` array. Just make sure the `number` field is unique.", 183 | "line": 19, 184 | "title": "store - cards state" 185 | }, 186 | { 187 | "file": "src/store/cardModule.js", 188 | "description": "The `getters` property contain getter functions for retrieve/query the state, for example: `voteCountForCard()`. Getter functions are called from components, or from `mutations`or `actions`.", 189 | "line": 54, 190 | "selection": { 191 | "start": { 192 | "line": 247, 193 | "character": 24 194 | }, 195 | "end": { 196 | "line": 247, 197 | "character": 31 198 | } 199 | }, 200 | "title": "store - getters" 201 | }, 202 | { 203 | "file": "src/store/cardModule.js", 204 | "description": "The `mutations` property contains functions to alter the state properties, for example: `addParticipantVoted()`. Mutation functions are called from `actions` functions.", 205 | "line": 86, 206 | "title": "store - mutations" 207 | }, 208 | { 209 | "file": "src/store/cardModule.js", 210 | "description": "The `actions` property contains functions that combines `getters` and `mutations` functions.\n\nThe `doVote` action is triggered when the participant selects a card. It adds the clientId to the `card.count` via the `addParticipantVoted` mutation and calls the `publishVoteToAbly` action to send the vote to the other connected clients.", 211 | "line": 110, 212 | "title": "store - actions, doVote" 213 | }, 214 | { 215 | "file": "src/store/realtimeModule.js", 216 | "description": "The `instantiateAblyConnection` action is responsible for:\n\n* Creating a new Ably.Realtime instance.\n* Updating the connection status.\n* Attaching this client to the Ably channel.\n* Retrieving the presence information (other connected clients) of the channel.\n* Subscribing to presence and voting information.", 217 | "line": 50, 218 | "selection": { 219 | "start": { 220 | "line": 271, 221 | "character": 5 222 | }, 223 | "end": { 224 | "line": 271, 225 | "character": 30 226 | } 227 | }, 228 | "title": "store - instantiateAblyConnection action" 229 | }, 230 | { 231 | "file": "src/store/realtimeModule.js", 232 | "description": "The `subscribeToAblyVoting()` action ensures that this client is listening to these named messages in the voting channel:\n\n* *vote*\n* *undo-vote*\n* *show-results*\n* *reset-voting*\n\nWhenever these messages are received a corresponding `actions` function is called to handle the message.", 233 | "line": 136, 234 | "title": "store - subscribeToAblyVoting action" 235 | }, 236 | { 237 | "file": "src/store/index.js", 238 | "description": "The `startSession()` action either uses the supplied `sessionId` from the route or generates a new `sessionId` via `generateSessionId()`. The `sessionId` is then stored in state via the `setSessionId` mutation.", 239 | "line": 42, 240 | "title": "store - startSession" 241 | }, 242 | { 243 | "file": "src/store/index.js", 244 | "description": "The `resetVoting` action calls the `commonResetVoting` action to clear out the clientIds from the card.count state and publishes a `reset-voting` message via the `publishResetVotingToAbly` action to trigger the reset at the other connected clients.", 245 | "line": 38, 246 | "selection": { 247 | "start": { 248 | "line": 421, 249 | "character": 28 250 | }, 251 | "end": { 252 | "line": 421, 253 | "character": 52 254 | } 255 | }, 256 | "title": "store - resetVoting action" 257 | }, 258 | { 259 | "file": "src/store/index.js", 260 | "description": "The `toggleShowResults` action is triggered by the Show/Hide Results button and toggles the visibility of the votes. The `toggleShowResults` mutation updates the state and the `publishShowResultsToAbly` action publishes a `show-results` message that is used by the connected clients to show/hide their local voting results.", 261 | "line": 51, 262 | "selection": { 263 | "start": { 264 | "line": 443, 265 | "character": 10 266 | }, 267 | "end": { 268 | "line": 443, 269 | "character": 34 270 | } 271 | }, 272 | "title": "store - toggleShowResults action" 273 | } 274 | ] 275 | } -------------------------------------------------------------------------------- /diagrams/functionality.svg: -------------------------------------------------------------------------------- 1 |
Planning Session
Yes
Select card
Votes visible?
Show/hide votes
Reset votes
Host visits website
Start session
Send URL to team members
Team member visits URL
-------------------------------------------------------------------------------- /diagrams/start-session.svg: -------------------------------------------------------------------------------- 1 | ClientVuexAzure FunctionAblyOther ClientsstartSessiongenerateSessionIdsessionIdstartSession(sessionId)alt[client is host][client is team member]instantiateAblyConnection(sessionId)createTokenRequest()createTokenRequest()tokentokennew RealtimeInstance(token)instancesetAblyClientIdattachToAblyChannels(channelName)channelsubscribeToAblyVoting()getExistingAblyPresenceSet()participantsaddParticipantJoinedloop[for eachparticipant]subscribeToAblyPresence()enterClientInAblyPresenceSet()numberOfParticipantsJoined()countbroadcast: participant enteredClientVuexAzure FunctionAblyOther Clients --------------------------------------------------------------------------------