├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── parser.yml ├── .gitignore ├── LICENSE ├── README.md ├── fpb.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── LangFilters.js ├── MarkdownParser.js ├── ParsedLink.js ├── SearchBar.js └── SearchResult.js ├── darkMode.js ├── img ├── moon.png ├── sun.jpg └── sun.png ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What does this PR do? 2 | Add feature(s) | Remove feature(s) | Fix | Add info 3 | 4 | ### Description 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/parser.yml: -------------------------------------------------------------------------------- 1 | name: free-programming-books-parse 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | reason: 7 | required: true 8 | default: "update json files" 9 | schedule: 10 | - cron: '0 0 * * *' 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout self 19 | uses: actions/checkout@v2 20 | with: 21 | path: json 22 | - name: Checkout programming books 23 | uses: actions/checkout@v2 24 | with: 25 | repository: EbookFoundation/free-programming-books 26 | path: fpb 27 | - name: Checkout parser 28 | uses: actions/checkout@v2 29 | with: 30 | repository: EbookFoundation/free-programming-books-parser 31 | path: parser 32 | 33 | - name: Use Node.js 34 | uses: actions/setup-node@v1 35 | with: 36 | node-version: '14.x' 37 | - run: npm install -g https://github.com/EbookFoundation/free-programming-books-parser 38 | - run: fpb-parse --output ./json/fpb.json 39 | - name: Commit Changes 40 | run: | 41 | cd './json' 42 | git config user.name 'github-actions[bot]' 43 | git config user.email 'github-actions[bot]@users.noreply.github.com' 44 | git add -f 'fpb.json' 45 | git commit -m "update fpb.json" 46 | - name: Push changes 47 | uses: ad-m/github-push-action@master 48 | with: 49 | directory: "./json" 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | ########## 4 | # GENERAL 5 | ########## 6 | 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.js 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | /build 17 | 18 | ########## 19 | # MACOS 20 | ########## 21 | 22 | # General 23 | .DS_Store 24 | .AppleDouble 25 | .LSOverride 26 | 27 | # Icon must end with two \r 28 | Icon 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ########## 50 | # WINDOWS 51 | ########## 52 | 53 | # Windows thumbnail cache files 54 | Thumbs.db 55 | Thumbs.db:encryptable 56 | ehthumbs.db 57 | ehthumbs_vista.db 58 | 59 | # Dump file 60 | *.stackdump 61 | 62 | # Folder config file 63 | [Dd]esktop.ini 64 | 65 | # Recycle Bin used on file shares 66 | $RECYCLE.BIN/ 67 | 68 | # Windows Installer files 69 | *.cab 70 | *.msi 71 | *.msix 72 | *.msm 73 | *.msp 74 | 75 | # Windows shortcuts 76 | *.lnk 77 | 78 | ########## 79 | # LINUX 80 | ########## 81 | 82 | *~ 83 | 84 | # temporary files which can be created if a process still has a handle open of a deleted file 85 | .fuse_hidden* 86 | 87 | # KDE directory preferences 88 | .directory 89 | 90 | # Linux trash folder which might appear on any partition or disk 91 | .Trash-* 92 | 93 | # .nfs files are created when an open file is removed but is still being accessed 94 | .nfs* 95 | 96 | ########## 97 | # VS CODE 98 | ########## 99 | 100 | .vscode/* 101 | !.vscode/settings.json 102 | !.vscode/tasks.json 103 | !.vscode/launch.json 104 | !.vscode/extensions.json 105 | !.vscode/*.code-snippets 106 | 107 | # Local History for Visual Studio Code 108 | .history/ 109 | 110 | # Built Visual Studio Code Extensions 111 | *.vsix 112 | 113 | ########## 114 | # VIM 115 | ########## 116 | 117 | # Swap 118 | [._]*.s[a-v][a-z] 119 | !*.svg # comment out if you don't need vector files 120 | [._]*.sw[a-p] 121 | [._]s[a-rt-v][a-z] 122 | [._]ss[a-gi-z] 123 | [._]sw[a-p] 124 | 125 | # Session 126 | Session.vim 127 | Sessionx.vim 128 | 129 | # Temporary 130 | .netrwhist 131 | *~ 132 | # Auto-generated tag files 133 | tags 134 | # Persistent undo 135 | [._]*.un~ 136 | 137 | ## ECLIPSE 138 | 139 | .metadata 140 | bin/ 141 | tmp/ 142 | *.tmp 143 | *.bak 144 | *.swp 145 | *~.nib 146 | local.properties 147 | .settings/ 148 | .loadpath 149 | .recommenders 150 | 151 | # External tool builders 152 | .externalToolBuilders/ 153 | 154 | # Locally stored "Eclipse launch configurations" 155 | *.launch 156 | 157 | # PyDev specific (Python IDE for Eclipse) 158 | *.pydevproject 159 | 160 | # CDT-specific (C/C++ Development Tooling) 161 | .cproject 162 | 163 | # CDT- autotools 164 | .autotools 165 | 166 | # Java annotation processor (APT) 167 | .factorypath 168 | 169 | # PDT-specific (PHP Development Tools) 170 | .buildpath 171 | 172 | # sbteclipse plugin 173 | .target 174 | 175 | # Tern plugin 176 | .tern-project 177 | 178 | # TeXlipse plugin 179 | .texlipse 180 | 181 | # STS (Spring Tool Suite) 182 | .springBeans 183 | 184 | # Code Recommenders 185 | .recommenders/ 186 | 187 | # Annotation Processing 188 | .apt_generated/ 189 | .apt_generated_test/ 190 | 191 | # Scala IDE specific (Scala & Java development for Eclipse) 192 | .cache-main 193 | .scala_dependencies 194 | .worksheet 195 | 196 | # Uncomment this line if you wish to ignore the project description file. 197 | # Typically, this file would be tracked if it contains build/dependency configurations: 198 | #.project 199 | 200 | ########## 201 | # NOTEPAD++ 202 | ########## 203 | 204 | # Notepad++ backups # 205 | *.bak 206 | 207 | ########## 208 | # NODE 209 | ########## 210 | 211 | # Logs 212 | logs 213 | *.log 214 | npm-debug.log* 215 | yarn-debug.log* 216 | yarn-error.log* 217 | lerna-debug.log* 218 | .pnpm-debug.log* 219 | 220 | # Diagnostic reports (https://nodejs.org/api/report.html) 221 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 222 | 223 | # Runtime data 224 | pids 225 | *.pid 226 | *.seed 227 | *.pid.lock 228 | 229 | # Directory for instrumented libs generated by jscoverage/JSCover 230 | lib-cov 231 | 232 | # Coverage directory used by tools like istanbul 233 | coverage 234 | *.lcov 235 | 236 | # nyc test coverage 237 | .nyc_output 238 | 239 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 240 | .grunt 241 | 242 | # Bower dependency directory (https://bower.io/) 243 | bower_components 244 | 245 | # node-waf configuration 246 | .lock-wscript 247 | 248 | # Compiled binary addons (https://nodejs.org/api/addons.html) 249 | build/Release 250 | 251 | # Dependency directories 252 | node_modules/ 253 | jspm_packages/ 254 | 255 | # Snowpack dependency directory (https://snowpack.dev/) 256 | web_modules/ 257 | 258 | # TypeScript cache 259 | *.tsbuildinfo 260 | 261 | # Optional npm cache directory 262 | .npm 263 | 264 | # Optional eslint cache 265 | .eslintcache 266 | 267 | # Optional stylelint cache 268 | .stylelintcache 269 | 270 | # Microbundle cache 271 | .rpt2_cache/ 272 | .rts2_cache_cjs/ 273 | .rts2_cache_es/ 274 | .rts2_cache_umd/ 275 | 276 | # Optional REPL history 277 | .node_repl_history 278 | 279 | # Output of 'npm pack' 280 | *.tgz 281 | 282 | # Yarn Integrity file 283 | .yarn-integrity 284 | 285 | # dotenv environment variable files 286 | .env 287 | .env.development.local 288 | .env.test.local 289 | .env.production.local 290 | .env.local 291 | 292 | # parcel-bundler cache (https://parceljs.org/) 293 | .cache 294 | .parcel-cache 295 | 296 | # Next.js build output 297 | .next 298 | out 299 | 300 | # Nuxt.js build / generate output 301 | .nuxt 302 | dist 303 | 304 | # Gatsby files 305 | .cache/ 306 | # Comment in the public line in if your project uses Gatsby and not Next.js 307 | # https://nextjs.org/blog/next-9-1#public-directory-support 308 | # public 309 | 310 | # vuepress build output 311 | .vuepress/dist 312 | 313 | # vuepress v2.x temp and cache directory 314 | .temp 315 | .cache 316 | 317 | # Docusaurus cache and generated files 318 | .docusaurus 319 | 320 | # Serverless directories 321 | .serverless/ 322 | 323 | # FuseBox cache 324 | .fusebox/ 325 | 326 | # DynamoDB Local files 327 | .dynamodb/ 328 | 329 | # TernJS port file 330 | .tern-port 331 | 332 | # Stores VSCode versions used for testing VSCode extensions 333 | .vscode-test 334 | 335 | # yarn v2 336 | .yarn/cache 337 | .yarn/unplugged 338 | .yarn/build-state.yml 339 | .yarn/install-state.gz 340 | .pnp.* 341 | 342 | ########## 343 | # BACKUP 344 | ########## 345 | 346 | *.bak 347 | *.gho 348 | *.ori 349 | *.orig 350 | *.tmp 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Free Ebook Foundation 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # free-programming-books-search 2 | 3 | The free-programming-books-search is a companion project of [free-programming-books](https://ebookfoundation.github.io/free-programming-books/). It allows users to search by book title or author and filter by language. The search index is updated once per day, so changes made on [free-programming-books](https://ebookfoundation.github.io/free-programming-books/) may not be immediately reflected. 4 | 5 | ## Contents 6 | 7 | - [Contents](#contents) 8 | - [How It All Works](#how-it-all-works) 9 | - [Installation](#installation) 10 | - [NPM Installation](#npm-installation) 11 | - [Running the Website](#running-the-website) 12 | - [Deployment](#deployment) 13 | - [FAQ](#faq) 14 | 15 | ## How It All Works 16 | 17 | 1. THERE IS NO DATABASE INVOLVED. Rather, the books are stored in a markdown on [free-programming-books](https://ebookfoundation.github.io/free-programming-books/) and is parsed daily by [free-programming-books-parser](https://github.com/EbookFoundation/free-programming-books-parser). The books and all info pertaining to them are stored in a JSON file called `fpb.json`. 18 | 19 | 2. This JSON is downloaded locally and searched locally when the actual search function is used. 20 | 21 | ## Installation 22 | 23 | ### NPM Installation 24 | 25 | 1. Make sure you have [Node.js](https://nodejs.org/en/) installed. If you already do, skip to [Running the Website](#running-the-website). 26 | 2. Otherwise, download the LTS installer from [Node.js](https://nodejs.org/en/) website. 27 | 3. Follow the instructions of the installer, make sure npm is listed as a package to be installed. 28 | 4. Click Install. 29 | 5. Verify that Node.Js has been installed by going to command line and typing in `node`. It should show the current version. 30 | 6. Close out of Node by either closing and reopening the command line or with Ctrl + C. 31 | 7. Make sure to check out the [NPM website](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) for more info. 32 | 33 | ### Running the Website 34 | 35 | 1. Make sure you have [Git](https://git-scm.com/downloads) installed. 36 | 2. Clone the repo from Github with Git. 37 | 3. Navigate to the folder using command line. A easy way is to type "`cd`" and then drag and drop the folder into command line. 38 | 4. Type `npm install`. 39 | 5. Type `npm install react-scripts`. 40 | 6. Type `npm start`. At this point, the command prompt should start up the server, and a tab in your default browser should open up to localhost. 41 | 42 | ## Deployment 43 | 44 | MAKE SURE YOU HAVE COMPLETED THE INSTALLATION STEPS FIRST! 45 | 46 | 1. First, make sure that you the local folder containing the files has a remote configured called "`origin`". 47 | 1. If you aren't sure, navigate to the folder using Git (type "`cd`", then drag and drop folder in to Git command line). 48 | 2. Type `git init`. 49 | 3. Type `git remote add origin `, replacing `` with the url of your github repository. 50 | 2. Now, run `npm install -g gh-pages`. 51 | 3. Run `npm run deploy`. 52 | 4. This should deploy your code to "`https://yourusername.github.io/free-programming-books-search/`". 53 | 54 | ## FAQ 55 | 56 | - What database are we using to store the books? 57 | - NONE! The books are stored in a JSON file which is downloaded locally. 58 | 59 | - I added a book but it's not showing up on search? 60 | - Give it some time. The parser is run once a day, so it may take up to 24 hours for the search to reflect that. 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://ebookfoundation.github.io/free-programming-books-search/", 3 | "name": "fpb_search_page", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^12.8.3", 10 | "axios": "^0.24.0", 11 | "fuse.js": "^6.4.6", 12 | "jQuery": "^1.7.4", 13 | "query-string": "^7.1.1", 14 | "react": "^17.0.2", 15 | "react-cookie": "^4.1.1", 16 | "react-dom": "^17.0.2", 17 | "react-markdown": "^8.0.3", 18 | "react-scripts": "^4.0.3", 19 | "rehype-raw": "^6.1.1", 20 | "rehype-slug": "^5.0.1", 21 | "web-vitals": "^1.1.2" 22 | }, 23 | "scripts": { 24 | "predeploy": "npm run build", 25 | "deploy": "gh-pages -d build", 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "gh-pages": "^3.2.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EbookFoundation/free-programming-books-search/7a8990c85c4315bf524c47a391b2f75b7a3f5f88/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | 25 | free-programming-books | Freely available programming books 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EbookFoundation/free-programming-books-search/7a8990c85c4315bf524c47a391b2f75b7a3f5f88/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EbookFoundation/free-programming-books-search/7a8990c85c4315bf524c47a391b2f75b7a3f5f88/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "FPB Search", 3 | "name": "Free Programming Books Search", 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 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | padding:50px; 4 | font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 5 | color:#595959; 6 | font-weight:400; 7 | } 8 | 9 | h1, h2, h3, h4, h5, h6 { 10 | color:#222; 11 | margin:0 0 20px; 12 | } 13 | 14 | p, ul, ol, table, pre, dl { 15 | margin:0 0 20px; 16 | } 17 | 18 | h1, h2, h3 { 19 | line-height:1.1; 20 | } 21 | 22 | h1 { 23 | font-size:28px; 24 | font-weight: bold; 25 | } 26 | 27 | h2 { 28 | color:#393939; 29 | font-weight: bold; 30 | } 31 | 32 | h3, h4, h5, h6 { 33 | color:#494949; 34 | font-weight: bold; 35 | } 36 | 37 | a { 38 | color:#267CB9; 39 | text-decoration:none; 40 | } 41 | 42 | a:hover { 43 | color:#069; 44 | font-weight: bold; 45 | } 46 | 47 | a small { 48 | font-size:11px; 49 | color:#777; 50 | margin-top:-0.3em; 51 | display:block; 52 | } 53 | 54 | a:hover small { 55 | color:#777; 56 | } 57 | 58 | .wrapper { 59 | width:860px; 60 | margin:0 auto; 61 | } 62 | 63 | blockquote { 64 | border-left:1px solid #e5e5e5; 65 | margin:0; 66 | padding:0 0 0 20px; 67 | font-style:italic; 68 | } 69 | 70 | code, pre { 71 | font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace; 72 | color:#333; 73 | } 74 | 75 | pre { 76 | padding:8px 15px; 77 | background: #f8f8f8; 78 | border-radius:5px; 79 | border:1px solid #e5e5e5; 80 | overflow-x: auto; 81 | } 82 | 83 | table { 84 | width:100%; 85 | border-collapse:collapse; 86 | } 87 | 88 | th, td { 89 | text-align:left; 90 | padding:5px 10px; 91 | border-bottom:1px solid #e5e5e5; 92 | } 93 | 94 | dt { 95 | color:#444; 96 | font-weight:500; 97 | } 98 | 99 | th { 100 | color:#444; 101 | } 102 | 103 | img { 104 | max-width:100%; 105 | cursor: pointer; 106 | } 107 | 108 | header { 109 | width:270px; 110 | float:left; 111 | position:fixed; 112 | -webkit-font-smoothing:subpixel-antialiased; 113 | background-color: white; 114 | z-index: 10; 115 | } 116 | 117 | header ul { 118 | list-style:none; 119 | height:40px; 120 | padding:0; 121 | background: #f4f4f4; 122 | border-radius:5px; 123 | border:1px solid #e0e0e0; 124 | width:270px; 125 | } 126 | 127 | header li { 128 | width:89px; 129 | float:left; 130 | border-right:1px solid #e0e0e0; 131 | height:40px; 132 | } 133 | 134 | header li:first-child a { 135 | border-radius:5px 0 0 5px; 136 | } 137 | 138 | header li:last-child a { 139 | border-radius:0 5px 5px 0; 140 | } 141 | 142 | header ul a { 143 | line-height:1; 144 | font-size:11px; 145 | color:#999; 146 | display:block; 147 | text-align:center; 148 | padding-top:6px; 149 | height:34px; 150 | } 151 | 152 | header ul a:hover { 153 | color:#999; 154 | } 155 | 156 | header ul a:active { 157 | background-color:#f0f0f0; 158 | } 159 | 160 | strong { 161 | color:#222; 162 | font-weight:500; 163 | } 164 | 165 | header ul li + li + li { 166 | border-right:none; 167 | width:89px; 168 | } 169 | 170 | header ul a strong { 171 | font-size:14px; 172 | display:block; 173 | color:#222; 174 | } 175 | 176 | section { 177 | width:500px; 178 | float:right; 179 | padding-bottom:50px; 180 | } 181 | 182 | section > section { 183 | float:none; 184 | padding-bottom:0; 185 | } 186 | 187 | small { 188 | font-size:11px; 189 | } 190 | 191 | hr { 192 | border:0; 193 | background:#e5e5e5; 194 | height:1px; 195 | margin:0 0 20px; 196 | } 197 | 198 | footer { 199 | width:270px; 200 | float:left; 201 | position:fixed; 202 | bottom:50px; 203 | -webkit-font-smoothing:subpixel-antialiased; 204 | } 205 | 206 | @media print, screen and (max-width: 960px) { 207 | 208 | div.wrapper { 209 | width:auto; 210 | margin:0; 211 | } 212 | 213 | header, section, footer { 214 | float:none; 215 | position:static; 216 | width:auto; 217 | } 218 | 219 | header { 220 | padding-right:0; 221 | } 222 | 223 | section { 224 | border:1px solid #e5e5e5; 225 | border-width:1px 0; 226 | padding:20px 0; 227 | margin:0 0 20px; 228 | } 229 | 230 | section > section { 231 | border:none; 232 | border-width:0; 233 | padding:0; 234 | margin:0; 235 | width:100%; 236 | } 237 | 238 | header a small { 239 | display:inline; 240 | } 241 | 242 | header ul { 243 | position:absolute; 244 | right:50px; 245 | top:52px; 246 | } 247 | } 248 | 249 | @media print, screen and (max-width: 720px) { 250 | body { 251 | word-wrap:break-word; 252 | } 253 | 254 | header { 255 | padding:0; 256 | } 257 | 258 | header ul, header p.view { 259 | position:static; 260 | } 261 | 262 | pre, code { 263 | word-wrap:normal; 264 | } 265 | } 266 | 267 | @media print, screen and (max-width: 480px) { 268 | body { 269 | padding:15px; 270 | } 271 | 272 | header ul { 273 | width:99%; 274 | } 275 | 276 | header li, header ul li + li + li { 277 | width:33%; 278 | } 279 | } 280 | 281 | @media print { 282 | body { 283 | padding:0.4in; 284 | font-size:12pt; 285 | color:#444; 286 | } 287 | } 288 | 289 | form { 290 | visibility: hidden; 291 | } 292 | 293 | .searchbar { 294 | padding-bottom: 0.3em; 295 | visibility: visible; 296 | } 297 | 298 | .searchterm { 299 | border: 1px solid #666; 300 | border-radius: 0.3em; 301 | padding: 0.15em 0.15em 0.15em 0.15em; 302 | width: 15em; 303 | } 304 | 305 | .languages { 306 | border: 1px solid #666; 307 | border-radius: 0.3em; 308 | padding: 0.15em 0.15em 0.15em 0.15em; 309 | width: 15.4em; 310 | visibility: visible; 311 | 312 | } 313 | 314 | .sect-drop { 315 | box-sizing: border-box; 316 | background-color: #222222; 317 | color: white; 318 | } 319 | 320 | #root { 321 | box-sizing: border-box; 322 | } 323 | 324 | 325 | .dark-content { 326 | background-color: black; 327 | color: #D4CECD 328 | } 329 | 330 | .dark-content h1, .dark-content h2, .dark-content h3, .dark-content h4, .dark-content h5, .dark-content h6 { 331 | color: #DDDDDD !important; 332 | } 333 | 334 | .dark-content small{ 335 | color: #A29D9C 336 | } 337 | 338 | .dark-content a{ 339 | color: #58a0d3; 340 | } 341 | 342 | 343 | 344 | .frontPage { 345 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 346 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 347 | sans-serif; 348 | -webkit-font-smoothing: antialiased; 349 | -moz-osx-font-smoothing: grayscale; 350 | display: flex; 351 | flex-direction: column; 352 | height: 100; 353 | text-align: center; 354 | justify-content: center; 355 | } 356 | 357 | .search-results { 358 | justify-content: center; 359 | align-items: flex-start; 360 | display: flex; 361 | flex-direction: row; 362 | flex-flow: row wrap; 363 | box-sizing: border-box; 364 | /* justify-content: normal ; */ 365 | align-content: flex-start; 366 | padding-left: 1em; 367 | padding-right: 1em; 368 | } 369 | 370 | .result { 371 | /* width: 30%; */ 372 | padding: 0.25em 0.5em; 373 | } 374 | 375 | 376 | .filters { 377 | margin-top: -1em; 378 | max-height: 13em; 379 | overflow: scroll; 380 | width: 18em; 381 | overflow-x: hidden; 382 | visibility: visible; 383 | } 384 | 385 | .filterHeader { 386 | display: flex; 387 | justify-content: left; 388 | } 389 | 390 | .filterHeader button { 391 | margin-left: 1em; 392 | width: 1.5em; 393 | height: 1.7em; 394 | cursor: pointer; 395 | } 396 | 397 | .langFilters { 398 | margin-top: 0.5em; 399 | } 400 | 401 | .filters input { 402 | cursor: pointer; 403 | } 404 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | import Fuse from "fuse.js"; 4 | import { useCookies } from "react-cookie"; 5 | import queryString from "query-string"; 6 | 7 | import LangFilters from "./components/LangFilters"; 8 | import SearchBar from "./components/SearchBar"; 9 | import SearchResult from "./components/SearchResult"; 10 | import MarkdownParser from "./components/MarkdownParser"; 11 | 12 | import SunImg from "./img/sun.png"; 13 | import MoonImg from "./img/moon.png"; 14 | import { ThemeContext, themes, swapMode } from "./darkMode"; 15 | 16 | function jsonToArray(json) { 17 | // list of all books 18 | let arr = []; 19 | // list of all topics (sections) 20 | let sections = []; 21 | // for each markdown document 22 | for (let i = 0; i < json.children.length; i++) { 23 | json.children[i].children.forEach((document) => { 24 | // for each topic in the markdown 25 | // these are typically h2 and h3 tags in the markdown 26 | document.sections.forEach((section) => { 27 | // Add section to master list if it's not there 28 | if (!sections.includes(section.section)) sections.push(section.section); 29 | // Add new entries that were under an h2 tag 30 | section.entries.forEach((entry) => { 31 | arr.push({ 32 | author: entry.author, 33 | title: entry.title, 34 | url: entry.url, 35 | lang: document.language, 36 | section: section.section, 37 | }); 38 | }); 39 | // Add new entries that were under an h3 tag 40 | section.subsections.forEach((subsection) => { 41 | subsection.entries.forEach((entry) => { 42 | arr.push({ 43 | author: entry.author, 44 | title: entry.title, 45 | url: entry.url, 46 | lang: document.language, 47 | section: section.section, 48 | subsection: subsection.section, 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | } 55 | return { arr: arr, sections: sections }; 56 | } 57 | 58 | function App() { 59 | // keeps the state of the json 60 | const [data, setData] = useState(undefined); 61 | // put all books into one array. uses more memory, but search is faster and less complex 62 | const [dataArray, setDataArray] = useState([]); 63 | // Keeps track if all resources are loaded 64 | const [loading, setLoading] = useState(true); 65 | // State keeping track of all search parameters 66 | // use the changeParameter function to set, NOT setSearchParams 67 | // changeParameter will retain the rest of the state 68 | let defaultSearch = queryString.parse(document.location.search).search || ""; 69 | const [searchParams, setSearchParams] = useState({ searchTerm: defaultSearch, "lang.code": "" }); 70 | // array of all search results 71 | const [searchResults, setSearchResults] = useState([]); 72 | const [cookies, setCookie, ] = useCookies(["lightMode"]); 73 | const [queries, setQueries] = useState({ lang: "", subject: "" }); 74 | 75 | // eslint-disable-next-line 76 | const [error, setError] = useState(""); 77 | 78 | let resultsList = null; // the html string containing the search results 79 | 80 | // Used to change the search parameters state 81 | // Heavily used in child components to set the state 82 | const changeParameter = (param, value) => { 83 | setSearchParams({ ...searchParams, [param]: value }); 84 | }; 85 | 86 | // fetches data the first time the page renders 87 | useEffect(() => { 88 | swapMode(cookies.lightMode ? themes.lightMode : themes.darkMode); 89 | async function fetchData() { 90 | try { 91 | setQueries(queryString.parse(document.location.search)); 92 | if (queries.lang) { 93 | if (queries.lang === "langs" || queries.lang === "subjects") { 94 | changeParameter("lang.code", "en"); 95 | } else { 96 | changeParameter("lang.code", queries.lang); 97 | } 98 | } 99 | // setLoading(true); 100 | let result = await axios.get( 101 | "https://raw.githubusercontent.com/EbookFoundation/free-programming-books-search/main/fpb.json" 102 | ); 103 | setData(result.data); 104 | // eslint-disable-next-line 105 | let { arr, sections } = jsonToArray(result.data); 106 | setDataArray(arr); 107 | } catch (e) { 108 | setError("Couldn't get data. Please try again later") 109 | } 110 | setLoading(false); 111 | } 112 | fetchData(); 113 | }, []); 114 | 115 | // fires when searchTerm changes 116 | // Finds most relevant title or author 117 | // THIS IS THE MAIN SEARCH FUNCTION 118 | useEffect(() => { 119 | if (dataArray) { 120 | const fuseOptions = { 121 | useExtendedSearch: true, // see fuse.js documentation 122 | findAllMatches: true, //continue searching after first match 123 | shouldSort: true, // sort by proximity score 124 | includeScore: true, // includes score in results 125 | includeMatches: true, 126 | threshold: 0.2, // threshold for fuzzy-search, 127 | keys: ["author", "title", "lang.code", "section"], 128 | }; 129 | 130 | // create new fuse given the array of books and the fuse options from above 131 | let fuse = new Fuse(dataArray, fuseOptions); 132 | let andQuery = []; // for filters that MUST be matched, like language 133 | let orQuery = []; // filters where any may be matched, like author or title 134 | 135 | // for each search param 136 | for (const [key, value] of Object.entries(searchParams)) { 137 | if (value === null || value === "") continue; 138 | if (key === "lang.code" || key === "section") { 139 | // the '^' means it must be an exact match at the beginning 140 | // this is because lang.code and section are strict filters 141 | andQuery.push({ [key]: `^${value}` }); 142 | } 143 | if (key === "searchTerm") { 144 | orQuery.push({ author: value }); 145 | orQuery.push({ title: value }); 146 | } 147 | } 148 | // Nest the 'or' query inside the 'and' query 149 | // Necessary step, a quirk with fuse.js 150 | andQuery.push({ $or: orQuery }); 151 | // Perform the search 152 | let result = fuse.search({ 153 | $and: andQuery, 154 | }); 155 | // filter to top results 156 | result = result.slice(0, 40); 157 | // console.log(result) 158 | 159 | // Goes through the list of results 160 | // let relevantLists = []; 161 | // result.forEach((entry) => { 162 | // // Checks if a new entry has already been made with the given programming language and human language. 163 | // let obj = relevantLists.find( 164 | // (o) => o.item.section === entry.item.section && o.item.lang.code === entry.item.lang.code 165 | // ); 166 | // if (!obj && entry.item.lang.code) { 167 | // let langCode = entry.item.lang.code; 168 | // let section = entry.item.subsection ? entry.item.subsection : entry.item.section; 169 | // // English is split into the subjects and langs file. The parser flags which type of entry it is to use here 170 | // if (langCode === "en") { 171 | // if (entry.item.lang.isSubject) { 172 | // langCode = "subjects"; 173 | // } else { 174 | // langCode = "langs"; 175 | // } 176 | // } 177 | 178 | // // Consider moving function out of here 179 | // let id = section; 180 | 181 | // // Some ids are in HTML tags, so this will extract that id to form proper links 182 | // if (id.includes("Loading...; 223 | // } 224 | if (error) { 225 | return

Error: {error}

; 226 | } 227 | if (searchParams.searchTerm && searchResults.length !== 0) { 228 | resultsList = 229 | searchResults && 230 | searchResults.map((entry) => { 231 | return ; 232 | }); 233 | } 234 | 235 | return ( 236 |
237 | 238 | {({ changeTheme }) => { 239 | let willBeDarkMode = cookies.lightMode && cookies.lightMode.toLowerCase() !== "true"; //whether or not we are currently light mode and will become dark mode 240 | changeTheme(willBeDarkMode ? themes.light : themes.dark); 241 | return ( 242 | Toggle light/dark mode { 246 | setCookie("lightMode", willBeDarkMode); 247 | changeTheme(willBeDarkMode ? themes.light : themes.dark); 248 | }} 249 | style={{ width: "20px", height: "20px", display: "block", marginLeft: "auto" }} 250 | /> 251 | ); 252 | }} 253 | 254 |
255 |

256 | free-programming-books 257 |

258 | 259 |

260 | :books:{" "} 268 | Freely available programming books 269 |

270 | 271 |

272 | 273 | View the Project on GitHub EbookFoundation/free-programming-books 274 | 275 |

276 |

277 | Does a link not work? 278 |
279 | 280 | Report an error on GitHub 281 | 282 |

283 | 284 |
285 | {loading ? ( 286 |

287 | ) : ( 288 |

289 | {" "} 290 | {" "} 291 |
292 | )} 293 |
294 |
295 | 296 |
297 | {loading ? ( 298 |

Loading

299 | ) : resultsList ? ( 300 |
301 |
302 |

Search Results

303 |
    {resultsList}
304 |
305 | ) : searchParams.searchTerm ? ( 306 |
307 |
308 |

No results found.

309 |
310 | ) : 311 | 312 | } 313 |
314 | 330 |
331 | ); 332 | } 333 | 334 | export default App; 335 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/LangFilters.js: -------------------------------------------------------------------------------- 1 | import { React, useState, useEffect } from "react"; 2 | import queryString from "query-string"; 3 | 4 | function LangFilters({ changeParameter, data, langCode }) { 5 | const [languages, setLanguages] = useState([]); 6 | const [selected, setSelected] = useState(langCode); 7 | const [showFilters, setShow] = useState(false); 8 | let options = null; 9 | 10 | const handleChange = (e) => { 11 | changeParameter("lang.code", e.target.value); 12 | setSelected(e.target.value); 13 | }; 14 | 15 | useEffect(() => { 16 | let queries = queryString.parse(document.location.search); 17 | if (queries.lang) { 18 | if (queries.lang === "langs" || queries.lang === "subjects") { 19 | changeParameter("lang.code", "en"); 20 | setSelected("en"); 21 | } else { 22 | changeParameter("lang.code", queries.lang); 23 | setSelected(queries.lang); 24 | } 25 | } else { 26 | changeParameter("lang.code", ""); 27 | setSelected("") 28 | } 29 | }, []); 30 | 31 | useEffect( 32 | // run whenever data changes 33 | () => { 34 | if (data) { 35 | let langArray = [{ code: "en", name: "English" }]; 36 | data.children[0].children.forEach((document) => { 37 | if (typeof document.language.name === "string" && document.language.name.length > 0) { 38 | //make sure the language is valid and not blank 39 | //console.log("LANGUAGE: " + document.language.name) 40 | if (document.language.code !== "en") { 41 | // used to ensure only one English is listed 42 | langArray.push(document.language); 43 | } 44 | } 45 | }); 46 | langArray.sort((a, b) => a.name > b.name); 47 | setLanguages(langArray); 48 | } 49 | }, 50 | [data] 51 | ); 52 | 53 | const createOption = (language) => { 54 | return ( 55 |
56 | 67 |
68 | ); 69 | }; 70 | 71 | options = 72 | languages && 73 | languages.map((language) => { 74 | return createOption(language); 75 | }); 76 | 77 | let filterList = ( 78 |
79 | 90 | {options} 91 |
92 | ); 93 | 94 | return ( 95 |
96 |
97 |

Filter by Language

98 | 99 |
100 | {showFilters ? filterList : ""} 101 |
102 | ); 103 | } 104 | 105 | export default LangFilters; 106 | -------------------------------------------------------------------------------- /src/components/MarkdownParser.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | 4 | import ReactMarkdown from "react-markdown"; 5 | import rehypeSlug from "rehype-slug"; 6 | import rehypeRaw from "rehype-raw"; 7 | 8 | import ParsedLink from "./ParsedLink"; 9 | 10 | function MarkdownParser({ file, sect }) { 11 | let [markdown, setMarkdown] = useState(null); 12 | const [loading, setLoading] = useState(true); 13 | 14 | useEffect(() => { 15 | async function fetchData() { 16 | try { 17 | // console.log({sect: sect, file: file}); 18 | setLoading(true); 19 | let result = null; 20 | if (sect && file) { 21 | // Both sect and file exist so construct the URL with both parameters 22 | result = await axios.get( 23 | `https://raw.githubusercontent.com/EbookFoundation/free-programming-books/main/${sect}/${file}` 24 | ); 25 | } else if (!sect && file) { 26 | // Occurs when getting a file from the root directory 27 | result = await axios.get( 28 | `https://raw.githubusercontent.com/EbookFoundation/free-programming-books/main/${file}` 29 | ); 30 | } else { 31 | // Default to getting the README 32 | result = await axios.get( 33 | `https://raw.githubusercontent.com/EbookFoundation/free-programming-books/main/README.md` 34 | ); 35 | } 36 | 37 | setMarkdown(result.data); 38 | } catch (e) { 39 | console.log("Couldn't get data. Please try again later"); 40 | } 41 | setLoading(false); 42 | } 43 | fetchData(); 44 | }, [file, sect]); 45 | 46 | if (loading) { 47 | return

Loading...

; 48 | } 49 | 50 | if (!markdown) { 51 | return

Error: Could not retrieve data.

; 52 | } 53 | 54 | return ( 55 |
56 | 67 | {children} 68 | 69 | ); 70 | } 71 | return ; 72 | }, 73 | }} 74 | /> 75 |
76 | ); 77 | } 78 | 79 | export default MarkdownParser; 80 | -------------------------------------------------------------------------------- /src/components/ParsedLink.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | function ParsedLink({ children, sect, href, id }) { 4 | const [folder, setFolder] = useState(null); 5 | const [file, setFile] = useState(null); 6 | 7 | useEffect(() => { 8 | // Splits the original link into the folder and file names 9 | // If there is only one entry then the folder is the root directory and the entry is the file 10 | let hrefSplit = href.split("/"); 11 | 12 | if (hrefSplit.length === 2) { 13 | // Some docs reference back to the root directory which would give the folder ".." 14 | // When that happens, skip setting the folder as it should stay null. 15 | if (hrefSplit[0] !== "..") { 16 | setFolder(hrefSplit[0]); 17 | } 18 | setFile(hrefSplit[1]); 19 | } else { 20 | // Only a file is given 21 | setFile(hrefSplit[0]); 22 | // When the current section is docs, all relative links stay in docs 23 | if (sect === "docs") { 24 | setFolder(sect); 25 | } else { 26 | setFolder(null); 27 | } 28 | } 29 | }, [href]); 30 | 31 | if (folder && file) { 32 | return {children}; 33 | } else if (file) { 34 | return {children}; 35 | } else { // Go to the homepage when there's a bad relative URL 36 | return {children} 37 | } 38 | } 39 | 40 | export default ParsedLink; 41 | -------------------------------------------------------------------------------- /src/components/SearchBar.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | 3 | function SearchBar(props) { 4 | useEffect(() => { 5 | document.getElementById("searchBar").value = props.defaultTerm 6 | }, []); 7 | 8 | const handleChange = (e) => { 9 | props.changeParameter("searchTerm", e.target.value); 10 | }; 11 | 12 | return ( 13 |
{ 15 | e.preventDefault(); 16 | }} 17 | name="searchBar" 18 | className="searchbar" 19 | > 20 | 29 |
30 | ); 31 | } 32 | 33 | export default SearchBar; 34 | -------------------------------------------------------------------------------- /src/components/SearchResult.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function SearchResult({ data }) { 4 | return ( 5 |
  • 6 | 7 | ({data.lang.code}) {data.title}{data.author ? ` by ${data.author}` : ""} 8 | 9 |
  • 10 | ); 11 | } 12 | 13 | export default SearchResult; 14 | -------------------------------------------------------------------------------- /src/darkMode.js: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext } from "react"; 2 | 3 | //https://levelup.gitconnected.com/dark-mode-in-react-533faaee3c6e 4 | 5 | export const themes = { 6 | dark: "", 7 | light: "dark-content", 8 | }; 9 | 10 | export const swapMode = (theme) => { 11 | switch (theme) { 12 | case themes.light: 13 | document.body.classList.add('dark-content'); 14 | setTimeout(() => { 15 | document.getElementsByClassName('header')[0].classList.add('dark-content'); 16 | }, 0); 17 | break; 18 | case themes.dark: 19 | default: 20 | document.body.classList.remove('dark-content'); 21 | setTimeout(() => { 22 | document.getElementsByClassName('header')[0].classList.remove('dark-content'); 23 | }, 0); 24 | break; 25 | } 26 | } 27 | 28 | export const ThemeContext = createContext({ 29 | theme: themes.dark, 30 | changeTheme: () => {}, 31 | }); 32 | 33 | export default function ThemeContextWrapper(props) { 34 | const [theme, setTheme] = useState(props.theme); 35 | 36 | function changeTheme(theme) { 37 | setTheme(theme); 38 | } 39 | // console.log(theme) 40 | swapMode(theme) 41 | 42 | return ( 43 | 44 | {props.children} 45 | 46 | ); 47 | } -------------------------------------------------------------------------------- /src/img/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EbookFoundation/free-programming-books-search/7a8990c85c4315bf524c47a391b2f75b7a3f5f88/src/img/moon.png -------------------------------------------------------------------------------- /src/img/sun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EbookFoundation/free-programming-books-search/7a8990c85c4315bf524c47a391b2f75b7a3f5f88/src/img/sun.jpg -------------------------------------------------------------------------------- /src/img/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EbookFoundation/free-programming-books-search/7a8990c85c4315bf524c47a391b2f75b7a3f5f88/src/img/sun.png -------------------------------------------------------------------------------- /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.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './App.css'; 4 | import App from './App'; 5 | import ThemeContextWrapper from './darkMode'; 6 | import reportWebVitals from './reportWebVitals'; 7 | import { CookiesProvider } from 'react-cookie'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 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'; 6 | --------------------------------------------------------------------------------