├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs └── hello-world-config.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── speechly_app_config.sal ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── Microphone.tsx ├── RepoList.tsx ├── SpeechApp.tsx ├── data.ts ├── filter.ts ├── index.css ├── index.tsx ├── logo.svg ├── parser.ts ├── react-app-env.d.ts ├── serviceWorker.ts └── setupTests.ts ├── tsconfig.json └── yarn.lock /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | persist-credentials: false 14 | - run: npm install --no-dev 15 | - run: npm run-script build 16 | env: 17 | REACT_APP_APP_ID: ${{ secrets.REACT_APP_APP_ID }} 18 | REACT_APP_LANGUAGE: ${{ secrets.REACT_APP_LANGUAGE }} 19 | - name: deploy 20 | uses: JamesIves/github-pages-deploy-action@releases/v3 21 | with: 22 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 23 | BRANCH: gh-pages 24 | FOLDER: build 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Speechly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo has been moved to https://github.com/speechly/speechly/tree/main/examples/react-client-example 2 | -------------------------------------------------------------------------------- /docs/hello-world-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speechly/react-example-repo-filtering/89ca3c15025c4136ab674f519c66a3ad3624662c/docs/hello-world-config.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voice-repo-filtering", 3 | "homepage": "https://speechly.github.io/react-example-repo-filtering", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@speechly/react-client": "0.0.2", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.3.2", 10 | "@testing-library/user-event": "^7.1.2", 11 | "@types/jest": "^24.0.0", 12 | "@types/node": "^12.0.0", 13 | "@types/react": "^16.9.0", 14 | "@types/react-dom": "^16.9.0", 15 | "react": "^16.13.1", 16 | "react-dom": "^16.13.1", 17 | "react-scripts": "3.4.3", 18 | "typescript": "~3.7.2" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speechly/react-example-repo-filtering/89ca3c15025c4136ab674f519c66a3ad3624662c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Speechly React Example 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speechly/react-example-repo-filtering/89ca3c15025c4136ab674f519c66a3ad3624662c/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speechly/react-example-repo-filtering/89ca3c15025c4136ab674f519c66a3ad3624662c/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /speechly_app_config.sal: -------------------------------------------------------------------------------- 1 | # Which languages we can filter by 2 | languages = [ 3 | Go 4 | TypeScript 5 | Python 6 | ] 7 | 8 | # Which fields we can sort by 9 | sort_fields = [ 10 | name 11 | description 12 | language 13 | followers 14 | stars 15 | forks 16 | ] 17 | 18 | # Synonyms for "repo" 19 | results = [ 20 | items 21 | results 22 | repos 23 | repositories 24 | ] 25 | 26 | # A couple of commands for filtering. 27 | # 28 | # This will expand into e.g. following examples (not exhaustive): 29 | # "Show all Go repos" 30 | # "Show me only TypeScript repositories" 31 | # "Show Python results" 32 | # etc. 33 | # 34 | # Words in curly brackets ("{me}") are optional. 35 | # Square brackets are for lists (e.g. one option from the list may be used) 36 | *filter show {me} {[all | only]} $languages(language) {$results} 37 | *filter filter {$results} by $languages(language) {language} 38 | 39 | # A command for sorting, e.g.: 40 | # "Sort the repos by name" 41 | # "Order results by forks" 42 | # etc. 43 | *sort [sort | order] {the} {$results} by $sort_fields(sort_field) 44 | 45 | # A command for resetting the filters, e.g.: 46 | # "Reset all filters to default" 47 | # "Remove the filters" 48 | # "Reset to default" 49 | # etc. 50 | *reset [reset | remove] {[the | all]} {filters} {to default} 51 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .block { 2 | margin: 1em; 3 | } 4 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SpeechProvider } from "@speechly/react-client"; 3 | 4 | import "./App.css"; 5 | 6 | import { SpeechApp } from "./SpeechApp"; 7 | 8 | function App() { 9 | const appId = process.env.REACT_APP_APP_ID ?? ""; 10 | if (appId === undefined) { 11 | throw Error("Missing Speechly app ID!"); 12 | } 13 | 14 | const language = process.env.REACT_APP_LANGUAGE ?? ""; 15 | if (language === undefined) { 16 | throw Error("Missing Speechly app language!"); 17 | } 18 | 19 | return ( 20 |
21 |
22 |

Speechly React example app

23 |
24 | 25 |
26 | This is an example app for filtering data using{" "} 27 | Speechly and{" "} 28 | React. Check out the source code on{" "} 29 | 30 | GitHub 31 | 32 |
33 | 34 |
35 | Try filtering the repos by language by pressing the "Start" button and 36 | saying e.g.: 37 |
    38 |
  • 39 | Show me Go repos 40 |
  • 41 |
  • 42 | Show all TypeScript repositories 43 |
  • 44 |
45 | You can also sort by saying, e.g.: 46 |
    47 |
  • 48 | Sort by stars 49 |
  • 50 |
  • 51 | Order by name 52 |
  • 53 |
54 | If you want to reset the filters, just say Reset the filters. 55 |
56 | 57 | 58 | 59 | 60 |
61 | ); 62 | } 63 | 64 | export default App; 65 | -------------------------------------------------------------------------------- /src/Microphone.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Word as SpeechWord, 4 | SpeechSegment, 5 | SpeechState, 6 | } from "@speechly/react-client"; 7 | 8 | type Props = { 9 | segment?: SpeechSegment; 10 | state: SpeechState; 11 | onRecord: () => Promise; 12 | }; 13 | 14 | export const Microphone = React.memo( 15 | ({ state, segment, onRecord }: Props): JSX.Element => { 16 | let enabled = false; 17 | let text = "Error"; 18 | 19 | switch (state) { 20 | case SpeechState.Idle: 21 | case SpeechState.Ready: 22 | enabled = true; 23 | text = "Start"; 24 | break; 25 | case SpeechState.Recording: 26 | enabled = true; 27 | text = "Stop"; 28 | break; 29 | case SpeechState.Connecting: 30 | case SpeechState.Loading: 31 | enabled = false; 32 | text = "Loading..."; 33 | break; 34 | } 35 | 36 | return ( 37 |
38 | 41 | 42 |
43 | ); 44 | } 45 | ); 46 | 47 | const Transcript = React.memo( 48 | ({ segment }: { segment?: SpeechSegment }): JSX.Element => { 49 | if (segment === undefined) { 50 | return ( 51 |
52 | Waiting for speech input... 53 |
54 | ); 55 | } 56 | 57 | return ( 58 |
59 | {segment.words.map((w) => ( 60 | 61 | ))} 62 |
63 | ); 64 | } 65 | ); 66 | 67 | const Word = React.memo( 68 | ({ word }: { word: SpeechWord }): JSX.Element => { 69 | if (word.isFinal) { 70 | return {`${word.value} `}; 71 | } 72 | 73 | return {`${word.value} `}; 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /src/RepoList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Repository } from "./data"; 4 | 5 | type Props = { 6 | repos: Repository[]; 7 | }; 8 | 9 | export const RepoList = ({ repos }: Props): JSX.Element => { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {repos.map((repo) => ( 25 | 26 | ))} 27 | 28 |
NameLanguageDescriptionFollowersStarsForks
29 |
30 | ); 31 | }; 32 | 33 | const RepoRow = React.memo( 34 | ({ repo }: { repo: Repository }): JSX.Element => { 35 | return ( 36 | 37 | {repo.name} 38 | {repo.language} 39 | {repo.description} 40 | {repo.followers} 41 | {repo.stars} 42 | {repo.forks} 43 | 44 | ); 45 | } 46 | ); 47 | -------------------------------------------------------------------------------- /src/SpeechApp.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { SpeechSegment, useSpeechContext } from "@speechly/react-client"; 3 | 4 | import { repositories, Repository } from "./data"; 5 | import { Filter, filterRepos } from "./filter"; 6 | import { 7 | IntentType, 8 | SortEntityType, 9 | parseIntent, 10 | parseLanguageEntity, 11 | parseSortEntity, 12 | } from "./parser"; 13 | 14 | import { RepoList } from "./RepoList"; 15 | import { Microphone } from "./Microphone"; 16 | 17 | export const SpeechApp: React.FC = (): JSX.Element => { 18 | const [filter, setFilter] = useState(defaultFilter); 19 | const [repos, setRepos] = useState(repositories); 20 | 21 | const { toggleRecording, speechState, segment } = useSpeechContext(); 22 | 23 | useEffect(() => { 24 | if (segment === undefined) { 25 | return; 26 | } 27 | 28 | const nextFilter = { 29 | ...filter, 30 | ...parseSegment(segment), 31 | }; 32 | 33 | setFilter(nextFilter); 34 | setRepos(filterRepos(repositories, nextFilter)); 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, [segment]); 37 | 38 | return ( 39 |
40 | 45 | 46 |
47 | ); 48 | }; 49 | 50 | const emptyFilter: Filter = {}; 51 | const defaultFilter: Filter = { 52 | languages: [], 53 | sortBy: SortEntityType.Name, 54 | }; 55 | 56 | function parseSegment(segment: SpeechSegment): Filter { 57 | const intent = parseIntent(segment); 58 | 59 | switch (intent) { 60 | case IntentType.Filter: 61 | const languages = parseLanguageEntity(segment); 62 | 63 | if (languages.length === 0) { 64 | return emptyFilter; 65 | } 66 | 67 | return { 68 | languages, 69 | }; 70 | case IntentType.Sort: 71 | const sortBy = parseSortEntity(segment); 72 | if (sortBy !== SortEntityType.Unknown) { 73 | return { 74 | sortBy, 75 | }; 76 | } 77 | 78 | return emptyFilter; 79 | case IntentType.Reset: 80 | return defaultFilter; 81 | default: 82 | return emptyFilter; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/data.ts: -------------------------------------------------------------------------------- 1 | export type Repository = { 2 | name: string; 3 | description: string; 4 | language: string; 5 | followers: number; 6 | stars: number; 7 | forks: number; 8 | }; 9 | 10 | export const repositories: Repository[] = [ 11 | { 12 | name: "microsoft/typescript", 13 | description: 14 | "TypeScript is a superset of JavaScript that compiles to clean JavaScript output", 15 | language: "TypeScript", 16 | followers: 2200, 17 | stars: 65000, 18 | forks: 8700, 19 | }, 20 | { 21 | name: "nestjs/nest", 22 | description: 23 | "A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications on top of TypeScript & JavaScript (ES6, ES7, ES8)", 24 | language: "TypeScript", 25 | followers: 648, 26 | stars: 30900, 27 | forks: 2800, 28 | }, 29 | { 30 | name: "microsoft/vscode", 31 | description: "Visual Studio Code", 32 | language: "TypeScript", 33 | followers: 3000, 34 | stars: 105000, 35 | forks: 16700, 36 | }, 37 | { 38 | name: "denoland/deno", 39 | description: "A secure JavaScript and TypeScript runtime", 40 | language: "TypeScript", 41 | followers: 1700, 42 | stars: 68000, 43 | forks: 3500, 44 | }, 45 | { 46 | name: "kubernetes/kubernetes", 47 | description: "Production-Grade Container Scheduling and Management", 48 | language: "Go", 49 | followers: 3300, 50 | stars: 70700, 51 | forks: 25500, 52 | }, 53 | { 54 | name: "moby/moby", 55 | description: 56 | "Moby Project - a collaborative project for the container ecosystem to assemble container-based systems", 57 | language: "Go", 58 | followers: 3200, 59 | stars: 58600, 60 | forks: 16900, 61 | }, 62 | { 63 | name: "gohugoio/hugo", 64 | description: "The world’s fastest framework for building websites", 65 | language: "Go", 66 | followers: 1000, 67 | stars: 47200, 68 | forks: 5400, 69 | }, 70 | { 71 | name: "grafana/grafana", 72 | description: 73 | "The tool for beautiful monitoring and metric analytics & dashboards for Graphite, InfluxDB & Prometheus & More", 74 | language: "Go", 75 | followers: 1300, 76 | stars: 37500, 77 | forks: 7600, 78 | }, 79 | { 80 | name: "pytorch/pytorch", 81 | description: 82 | "Tensors and Dynamic neural networks in Python with strong GPU acceleration", 83 | language: "Python", 84 | followers: 1600, 85 | stars: 43000, 86 | forks: 11200, 87 | }, 88 | { 89 | name: "tensorflow/tensorflow", 90 | description: "An Open Source Machine Learning Framework for Everyone", 91 | language: "Python", 92 | followers: 8300, 93 | stars: 149000, 94 | forks: 82900, 95 | }, 96 | { 97 | name: "django/django", 98 | description: "The Web framework for perfectionists with deadlines", 99 | language: "Python", 100 | followers: 2300, 101 | stars: 52800, 102 | forks: 22800, 103 | }, 104 | { 105 | name: "apache/airflow", 106 | description: 107 | "Apache Airflow - A platform to programmatically author, schedule, and monitor workflows", 108 | language: "Python", 109 | followers: 716, 110 | stars: 18500, 111 | forks: 7200, 112 | }, 113 | ]; 114 | -------------------------------------------------------------------------------- /src/filter.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from "./data"; 2 | import { SortEntityType } from "./parser"; 3 | 4 | export type Filter = { 5 | languages?: string[]; 6 | sortBy?: SortEntityType; 7 | }; 8 | 9 | export function filterRepos( 10 | input: Repository[], 11 | filters: Filter 12 | ): Repository[] { 13 | let output = input; 14 | 15 | const languages = filters.languages ?? []; 16 | if (languages.length > 0) { 17 | output = input.filter((repo) => 18 | languages.includes(repo.language.toLowerCase()) 19 | ); 20 | } 21 | 22 | if (filters.sortBy === undefined) { 23 | return output; 24 | } 25 | 26 | return output.sort((left, right) => { 27 | switch (filters.sortBy) { 28 | case SortEntityType.Name: 29 | return left.name.localeCompare(right.name); 30 | case SortEntityType.Description: 31 | return left.description.localeCompare(right.description); 32 | case SortEntityType.Language: 33 | return left.language.localeCompare(right.language); 34 | case SortEntityType.Followers: 35 | return compareNumber(left.followers, right.followers); 36 | case SortEntityType.Stars: 37 | return compareNumber(left.stars, right.stars); 38 | case SortEntityType.Forks: 39 | return compareNumber(left.forks, right.forks); 40 | } 41 | 42 | return 0; 43 | }); 44 | } 45 | 46 | function compareNumber(left: number, right: number) { 47 | if (left < right) { 48 | return -1; 49 | } 50 | 51 | if (left > right) { 52 | return 1; 53 | } 54 | 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { SpeechSegment } from "@speechly/react-client"; 2 | 3 | export enum IntentType { 4 | Unknown = "unknown", 5 | Sort = "sort", 6 | Filter = "filter", 7 | Reset = "reset", 8 | } 9 | 10 | export enum EntityType { 11 | Language = "language", 12 | SortField = "sort_field", 13 | } 14 | 15 | export enum SortEntityType { 16 | Unknown = "unknown", 17 | Name = "name", 18 | Description = "description", 19 | Language = "language", 20 | Followers = "followers", 21 | Stars = "stars", 22 | Forks = "forks", 23 | } 24 | 25 | const SpeechIntentValues = Object.values(IntentType) as string[]; 26 | const SortTypeValues = Object.values(SortEntityType) as string[]; 27 | 28 | export function parseIntent(segment: SpeechSegment): IntentType { 29 | const { intent } = segment; 30 | 31 | if (SpeechIntentValues.includes(intent.intent)) { 32 | return intent.intent as IntentType; 33 | } 34 | 35 | return IntentType.Unknown; 36 | } 37 | 38 | export function parseLanguageEntity(segment: SpeechSegment): string[] { 39 | const langs: string[] = []; 40 | 41 | for (const e of segment.entities) { 42 | if (e.type === EntityType.Language) { 43 | langs.push(e.value.toLowerCase()); 44 | } 45 | } 46 | 47 | return langs; 48 | } 49 | 50 | export function parseSortEntity(segment: SpeechSegment): SortEntityType { 51 | let s = SortEntityType.Unknown; 52 | 53 | for (const e of segment.entities) { 54 | const val = e.value.toLowerCase(); 55 | 56 | if (e.type === EntityType.SortField && SortTypeValues.includes(val)) { 57 | s = val as SortEntityType; 58 | } 59 | } 60 | 61 | return s; 62 | } 63 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------