├── .gitignore ├── .netlify ├── README.md ├── examples └── resume.pdf ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts └── export.js ├── src ├── components │ ├── canvas.js │ ├── daterange.js │ ├── example.js │ ├── page.js │ ├── skillstools.js │ └── summary_line.js ├── index.js └── registerServiceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # result output 4 | out/ 5 | 6 | src/components/personal/ 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /.netlify: -------------------------------------------------------------------------------- 1 | {"site_id":"25f02818-c760-40e3-9cba-b5058162549a","path":"build/"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-resume 📄 2 | ============== 3 | 4 | > Generate resumes using [React](https://github.com/facebook/react), [`puppeteer`](https://github.com/GoogleChrome/puppeteer), and [`styled-components`](https://github.com/styled-components/styled-components). 5 | 6 | Inspired by: https://github.com/salomonelli/best-resume-ever 7 | 8 | **HTML preview:** https://react-resume.netlify.com/ 9 | 10 | **PDF output:** [`examples/resume.pdf`](examples/resume.pdf) 11 | 12 | ## Included batteries: 13 | 14 | - [`create-react-app`](https://github.com/facebookincubator/create-react-app) 15 | + React 16 | + babel 17 | + webpack 18 | + eslint 19 | + and other cool stuff that `create-react-app` provides. 20 | - [`puppeteer`](https://github.com/GoogleChrome/puppeteer) 21 | - [`styled-components`](https://github.com/styled-components/styled-components) 22 | 23 | Usage 24 | ===== 25 | 26 | 1. Clone this repository: `git clone https://github.com/dashed/react-resume.git` 27 | 28 | 2. Run `yarn install` (or `npm install`) 29 | 30 | 3. Run `create-react-app` in development mode: `yarn start` (or `npm start`) 31 | 32 | 4. Open http://localhost:3000 to view it in the browser. 33 | 34 | 5. Edit `src/` 35 | 36 | 6. Export PDF: `yarn pdf` (or `npm run pdf`) 37 | 38 | 7. Generated resume is in: `out/resume.pdf` 39 | 40 | License 41 | ======= 42 | 43 | MIT. 44 | -------------------------------------------------------------------------------- /examples/resume.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashed/react-resume/408fd4d814d8811266f3e872eca716faad4252d2/examples/resume.pdf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-resume", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "invariant": "^2.2.2", 7 | "normalize.css": "^7.0.0", 8 | "react": "^16.0.0", 9 | "react-dom": "^16.0.0", 10 | "react-scripts": "^1.1.0", 11 | "sanitize.css": "^5.0.0", 12 | "styled-components": "^2.4.0", 13 | "styled-components-tachyons": "^0.0.5" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject", 20 | "pdf": "node scripts/export.js", 21 | "pretty": "prettier --write --tab-width 4 'src/**/*.js' 'scripts/**/*.js'" 22 | }, 23 | "devDependencies": { 24 | "mkdirp": "^0.5.1", 25 | "prettier": "^1.12.1", 26 | "puppeteer": "^1.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashed/react-resume/408fd4d814d8811266f3e872eca716faad4252d2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | react-resume 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/export.js: -------------------------------------------------------------------------------- 1 | // node imports 2 | 3 | const http = require("http"); 4 | const path = require("path"); 5 | 6 | // 3rd-party imports 7 | 8 | const mkdirp = require("mkdirp"); 9 | const puppeteer = require("puppeteer"); 10 | 11 | // export code 12 | 13 | const SERVER = "http://localhost:3000/"; 14 | const OUT_DIR = path.join(__dirname, "../out/"); 15 | const PDF_FILENAME = "resume.pdf"; 16 | 17 | function sleep(ms = 0) { 18 | return new Promise(r => setTimeout(r, ms)); 19 | } 20 | 21 | const fetchResponse = () => { 22 | return new Promise((resolve, reject) => { 23 | try { 24 | const request = http.request(SERVER, response => 25 | resolve(response.statusCode) 26 | ); 27 | request.on("error", err => reject(err)); 28 | request.end(); 29 | } catch (err) { 30 | reject(err); 31 | } 32 | }); 33 | }; 34 | 35 | const reachableServer = () => { 36 | console.log("Connected to server ..."); 37 | 38 | return fetchResponse().then(statusCode => { 39 | if (statusCode === 200) { 40 | // 200 OK 41 | return true; 42 | } 43 | 44 | throw Error( 45 | `Unable to connect to server. Received status code: ${statusCode}` 46 | ); 47 | }); 48 | }; 49 | 50 | const makeOutDir = () => { 51 | return new Promise((resolve, reject) => { 52 | mkdirp(OUT_DIR, err => { 53 | if (!err) { 54 | resolve(); 55 | return; 56 | } 57 | 58 | reject(err); 59 | }); 60 | }); 61 | }; 62 | 63 | const convert = async () => { 64 | await reachableServer(); 65 | 66 | console.log("Exporting ..."); 67 | 68 | const browser = await puppeteer.launch({ args: ["--no-sandbox"] }); 69 | 70 | const page = await browser.newPage(); 71 | 72 | await page.goto(SERVER, { waitUntil: "networkidle2" }); 73 | 74 | await makeOutDir(); 75 | 76 | await sleep(2000); 77 | 78 | await page.pdf({ 79 | path: path.join(OUT_DIR, PDF_FILENAME), 80 | format: "letter" 81 | }); 82 | 83 | await browser.close(); 84 | 85 | console.log("Finished exports."); 86 | }; 87 | 88 | convert().catch(reason => { 89 | console.error(`${reason}`); 90 | process.exit(1); 91 | }); 92 | -------------------------------------------------------------------------------- /src/components/canvas.js: -------------------------------------------------------------------------------- 1 | // 3rd-party imports 2 | 3 | import styled from "styled-components"; 4 | 5 | // component 6 | 7 | const Canvas = styled.div` 8 | min-width: fit-content; 9 | padding: 15px; 10 | 11 | background-color: #e9ecef; 12 | 13 | -webkit-print-color-adjust: exact; 14 | print-color-adjust: exact; 15 | 16 | @media print { 17 | display: block; 18 | 19 | background-color: #fff; 20 | } 21 | `; 22 | 23 | export default Canvas; 24 | -------------------------------------------------------------------------------- /src/components/daterange.js: -------------------------------------------------------------------------------- 1 | // 3rd-party imports 2 | 3 | import React from "react"; 4 | import invariant from "invariant"; 5 | 6 | import styled from "styled-components"; 7 | 8 | // component 9 | 10 | const NOT_SET = {}; 11 | 12 | const Container = styled.span``; 13 | 14 | const DateRange = ({ start, end = NOT_SET }) => { 15 | invariant(start, "expected start"); 16 | 17 | if (end === NOT_SET) { 18 | return ( 19 | {`${start}\u00A0\u00A0\u2014\u00A0\u00A0Present`} 21 | ); 22 | } 23 | 24 | return ( 25 | {`${start}\u00A0\u00A0\u2014\u00A0\u00A0${end}`} 26 | ); 27 | }; 28 | 29 | export default DateRange; 30 | -------------------------------------------------------------------------------- /src/components/example.js: -------------------------------------------------------------------------------- 1 | // 3rd-party imports 2 | 3 | import React from "react"; 4 | 5 | import styled, { injectGlobal } from "styled-components"; 6 | 7 | import "styled-components-tachyons/variables.css"; 8 | import tachyons from "styled-components-tachyons"; 9 | 10 | // local imports 11 | 12 | import PageBase from "./page"; 13 | import Canvas from "./canvas"; 14 | 15 | import DateRange from "./daterange"; 16 | import SkillsTools from "./skillstools"; 17 | import SummaryLine from "./summary_line"; 18 | 19 | // global styles 20 | 21 | injectGlobal` 22 | @import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,200i,300,300i,400,400i,600,600i,700,700i,900,900i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext,vietnamese'); 23 | 24 | @import url('https://fonts.googleapis.com/css?family=Source+Code+Pro'); 25 | 26 | body { 27 | font-family: 'Source Sans Pro', sans-serif; 28 | font-size: 13px; 29 | 30 | font-kerning: normal; 31 | } 32 | 33 | a { 34 | color: #0366d6; 35 | text-decoration: none; 36 | } 37 | `; 38 | 39 | // components 40 | 41 | const Page = PageBase.extend` 42 | padding: 0.5in; 43 | 44 | display: flex; 45 | flex-direction: column; 46 | `; 47 | 48 | const Header = styled.div` 49 | ${tachyons}; 50 | 51 | font-weight: bold; 52 | font-size: 32px; 53 | text-align: center; 54 | `; 55 | 56 | const Contact = styled.div` 57 | ${tachyons}; 58 | 59 | display: flex; 60 | `; 61 | 62 | const ContactGutter = styled.div` 63 | flex-grow: 10; 64 | `; 65 | 66 | const Link = styled.a` 67 | font-family: "Source Code Pro", monospace; 68 | 69 | font-weight: 500; 70 | `; 71 | 72 | const Phone = styled.span` 73 | font-family: "Source Code Pro", monospace; 74 | 75 | font-weight: 500; 76 | `; 77 | 78 | const SubHeader = styled.div` 79 | ${tachyons}; 80 | font-weight: bold; 81 | font-size: 18px; 82 | `; 83 | 84 | const WorkExperience = styled.div` 85 | ${tachyons}; 86 | `; 87 | 88 | const LocationTime = styled.div` 89 | ${tachyons}; 90 | display: flex; 91 | `; 92 | 93 | const LocationTimeGutter = styled.div` 94 | flex-grow: 10; 95 | `; 96 | 97 | const Position = styled.div` 98 | ${tachyons}; 99 | font-style: italic; 100 | `; 101 | 102 | const SubSubHeader = styled.div` 103 | ${tachyons}; 104 | font-weight: bold; 105 | `; 106 | 107 | const Education = styled.div``; 108 | 109 | const EducationLine = styled.div` 110 | ${tachyons}; 111 | display: flex; 112 | `; 113 | 114 | const EducationGutter = styled.div` 115 | flex-grow: 10; 116 | `; 117 | 118 | const OpenSource = styled.div` 119 | ${tachyons}; 120 | `; 121 | 122 | const OpenSourceLine = styled.div` 123 | ${tachyons}; 124 | `; 125 | 126 | const Gutter = styled.div` 127 | flex-grow: 10; 128 | `; 129 | 130 | const Personal = () => { 131 | return ( 132 | 133 | 134 | {/* Header */} 135 | 136 |
{`Your Name`}
137 | 138 | 139 | {`E-mail:\u00A0\u00A0`} 140 | {`email@email.com`} 141 | 142 | {`Phone:\u00A0\u00A0`} 143 | {`+1-123-456-7890`} 144 | {`\u00A0\u00A0(mobile)`} 145 | 146 | 147 | {/* Work Experience */} 148 | 149 | {"Work Experience"} 150 | 151 | 152 | 153 | {"Company A"} 154 | 155 | 156 | 157 | 158 | {`Full-Stack Developer`} 159 | 160 | {`git, JavaScript, node.js, HTML, CSS`} 162 | 163 | {`Watched cat videos.`} 164 | 165 | {`Watched animals close-up with a wide-angle lens.`} 167 | 168 | {`Watched animals wearing hats close-up with a wide-angle lens.`} 170 | 171 | 172 | 173 | 174 | {"Company B"} 175 | 176 | 177 | 178 | 179 | {`Full-Stack Developer`} 180 | 181 | {`git, JavaScript, node.js, HTML, CSS`} 183 | 184 | {`Watched cat videos.`} 185 | 186 | {`Watched animals close-up with a wide-angle lens.`} 188 | 189 | {`Watched animals wearing hats close-up with a wide-angle lens.`} 191 | 192 | 193 | 194 | 195 | {"Company C"} 196 | 197 | 198 | 199 | 200 | {`Full-Stack Developer`} 201 | 202 | {`git, JavaScript, node.js, HTML, CSS`} 204 | 205 | {`Watched cat videos.`} 206 | 207 | {`Watched animals close-up with a wide-angle lens.`} 209 | 210 | {`Watched animals wearing hats close-up with a wide-angle lens.`} 212 | 213 | 214 | 215 | 216 | {"Company D"} 217 | 218 | 219 | 220 | 221 | {`Full-Stack Developer`} 222 | 223 | {`git, JavaScript, node.js, HTML, CSS`} 225 | 226 | {`Watched cat videos.`} 227 | 228 | {`Watched animals close-up with a wide-angle lens.`} 230 | 231 | {`Watched animals wearing hats close-up with a wide-angle lens.`} 233 | 234 | 235 | 236 | 237 | {"Company E"} 238 | 239 | 240 | 241 | 242 | {`Full-Stack Developer`} 243 | 244 | {`git, JavaScript, node.js, HTML, CSS`} 246 | 247 | {`Watched cat videos.`} 248 | 249 | {`Watched animals close-up with a wide-angle lens.`} 251 | 252 | {`Watched animals wearing hats close-up with a wide-angle lens.`} 254 | 255 | 256 | 257 | 258 |
{`1 of 2`}
259 |
260 | 261 | 262 | {"Technical Skills"} 263 | 264 | {`Programming languages (not full list):`} 267 | 268 | {`Proficient: Rust, C, Java, JavaScript, HTML, CSS, SQL, Clojure`} 270 | 271 | {`Knowledgeable: C++, Python, bash scripting, Go (golang), Haskell`} 273 | 274 | {`Tools, utilities, and other miscellaneous:`} 277 | 278 | {`Proficient: git (version control), Linux (and Unix), vim (and vi)`} 280 | 281 | {`Knowledgeable: Make, PostgreSQL, MySQL, Apache, nginx`} 283 | 284 | {`Web Development:`} 285 | 286 | {`Developed web applications using: Rust, node.js, Python, and Go (golang).`} 288 | 289 | {`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ultricies porttitor lobortis. Nullam et diam in lectus facilisis euismod. Sed in nulla leo.`} 291 | 292 | {`Nunc ullamcorper vel nunc in imperdiet. Etiam posuere orci a nisl egestas sollicitudin.`} 294 | 295 | {`Donec porta euismod nibh, ut pretium tellus sagittis et.`} 297 | 298 | {`Devops:`} 299 | 300 | {`Devops on: Heroku, Google Cloud Platform, Amazon Web Services (AWS).`} 302 | 303 | {`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ultricies porttitor lobortis. Nullam et diam in lectus facilisis euismod. Sed in nulla leo.`} 305 | 306 | {`Nunc ullamcorper vel nunc in imperdiet. Etiam posuere orci a nisl egestas sollicitudin.`} 308 | 309 | {`Donec porta euismod nibh, ut pretium tellus sagittis et.`} 311 | 312 | {`System Administration:`} 313 | 314 | {`Operating Systems: Ubuntu, Red Hat, Solaris.`} 316 | 317 | {`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ultricies porttitor lobortis. Nullam et diam in lectus facilisis euismod. Sed in nulla leo.`} 319 | 320 | {`Nunc ullamcorper vel nunc in imperdiet. Etiam posuere orci a nisl egestas sollicitudin.`} 322 | 323 | {`Donec porta euismod nibh, ut pretium tellus sagittis et.`} 325 | 326 | 327 | {"Projects"} 328 | 329 | 330 | 331 | 332 | {`Project A`} 333 | {`\u00A0\u00A0\u2014\u00A0\u00A0`} 334 | {`https://github.com/user/project_a`} 335 | 336 | 337 | 338 | {`Developed this project in my spare time because of reasons.`} 339 | 340 | 341 | 342 | 343 | 344 | {`Project B`} 345 | {`\u00A0\u00A0\u2014\u00A0\u00A0`} 346 | {`https://github.com/user/project_b`} 347 | 348 | 349 | 350 | {`Developed this project in my spare time because of reasons.`} 351 | 352 | 353 | 354 | 355 | 356 | {`Project C`} 357 | {`\u00A0\u00A0\u2014\u00A0\u00A0`} 358 | {`https://github.com/user/project_c`} 359 | 360 | 361 | 362 | {`Developed this project in my spare time because of reasons.`} 363 | 364 | 365 | 366 | 367 | 368 | {`Project D`} 369 | {`\u00A0\u00A0\u2014\u00A0\u00A0`} 370 | {`https://github.com/user/project_d`} 371 | 372 | 373 | 374 | {`Developed this project in my spare time because of reasons.`} 375 | 376 | 377 | 378 | 379 | 380 | {`Project E`} 381 | {`\u00A0\u00A0\u2014\u00A0\u00A0`} 382 | {`https://github.com/user/project_e`} 383 | 384 | 385 | 386 | {`Developed this project in my spare time because of reasons.`} 387 | 388 | 389 | 390 | 391 | {"Education"} 392 | 393 | 394 | 395 | 396 | {`University of Utopia`} 397 | 398 | {`City, State/Province, Country`} 399 | 400 | 401 | {`B.Sc. (Honours), Computer Science`} 402 | 403 | {`2017`} 404 | 405 | 406 | 407 | 408 | 409 |
{`2 of 2`}
410 |
411 |
412 | ); 413 | }; 414 | 415 | export default Personal; 416 | -------------------------------------------------------------------------------- /src/components/page.js: -------------------------------------------------------------------------------- 1 | // 3rd-party imports 2 | 3 | import styled from "styled-components"; 4 | 5 | // component 6 | 7 | const Page = styled.div` 8 | width: 8.5in; 9 | height: 11in; 10 | 11 | margin-right: auto; 12 | margin-left: auto; 13 | 14 | position: relative; 15 | overflow: hidden; 16 | 17 | background-color: ${({ bgcolor }) => (bgcolor ? bgcolor : "#fff")}; 18 | box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, 0.2); 19 | 20 | margin-top: 15px; 21 | margin-bottom: 15px; 22 | 23 | &:first-of-type { 24 | margin-top: 0px; 25 | } 26 | 27 | &:last-of-type { 28 | margin-bottom: 0px; 29 | } 30 | 31 | @media print { 32 | box-shadow: none; 33 | 34 | margin: 0; 35 | box-sizing: border-box; 36 | page-break-after: always; 37 | 38 | &:first-of-type { 39 | margin: 0; 40 | } 41 | 42 | &:last-of-type { 43 | margin: 0; 44 | } 45 | } 46 | `; 47 | 48 | export default Page; 49 | -------------------------------------------------------------------------------- /src/components/skillstools.js: -------------------------------------------------------------------------------- 1 | // 3rd-party imports 2 | 3 | import React from "react"; 4 | 5 | import styled from "styled-components"; 6 | import tachyons from "styled-components-tachyons"; 7 | 8 | // component 9 | 10 | const Container = styled.div` 11 | ${tachyons}; 12 | `; 13 | 14 | const SkillsTools = ({ children }) => { 15 | return ( 16 | 17 | {`Skills / Tools used:\u00A0\u00A0`} 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | export default SkillsTools; 24 | -------------------------------------------------------------------------------- /src/components/summary_line.js: -------------------------------------------------------------------------------- 1 | // 3rd-party imports 2 | 3 | import React from "react"; 4 | 5 | import styled from "styled-components"; 6 | import tachyons from "styled-components-tachyons"; 7 | 8 | // component 9 | 10 | const Container = styled.div` 11 | ${tachyons}; 12 | display: flex; 13 | text-align: justify; 14 | `; 15 | 16 | const Bullet = styled.div` 17 | ${tachyons}; 18 | `; 19 | 20 | const SummaryLine = ({ children }) => { 21 | return ( 22 | 23 | {`\u2014`} 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | export default SummaryLine; 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // 3rd-party imports 2 | 3 | import "normalize.css"; 4 | import "sanitize.css"; 5 | 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | // local imports 10 | 11 | import registerServiceWorker from "./registerServiceWorker"; 12 | 13 | const customResolve = path => { 14 | return import(`${path}`); 15 | }; 16 | 17 | customResolve("./components/personal/personal") 18 | .then(({ default: Resume }) => { 19 | ReactDOM.render(, document.getElementById("root")); 20 | 21 | registerServiceWorker(); 22 | }) 23 | .catch(reason => { 24 | import("./components/example").then(({ default: Resume }) => { 25 | ReactDOM.render(, document.getElementById("root")); 26 | 27 | registerServiceWorker(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener("load", () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === "installed") { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log( 60 | "New content is available; please refresh." 61 | ); 62 | } else { 63 | // At this point, everything has been precached. 64 | // It's the perfect time to display a 65 | // "Content is cached for offline use." message. 66 | console.log("Content is cached for offline use."); 67 | } 68 | } 69 | }; 70 | }; 71 | }) 72 | .catch(error => { 73 | console.error("Error during service worker registration:", error); 74 | }); 75 | } 76 | 77 | function checkValidServiceWorker(swUrl) { 78 | // Check if the service worker can be found. If it can't reload the page. 79 | fetch(swUrl) 80 | .then(response => { 81 | // Ensure service worker exists, and that we really are getting a JS file. 82 | if ( 83 | response.status === 404 || 84 | response.headers.get("content-type").indexOf("javascript") === 85 | -1 86 | ) { 87 | // No service worker found. Probably a different app. Reload the page. 88 | navigator.serviceWorker.ready.then(registration => { 89 | registration.unregister().then(() => { 90 | window.location.reload(); 91 | }); 92 | }); 93 | } else { 94 | // Service worker found. Proceed as normal. 95 | registerValidSW(swUrl); 96 | } 97 | }) 98 | .catch(() => { 99 | console.log( 100 | "No internet connection found. App is running in offline mode." 101 | ); 102 | }); 103 | } 104 | 105 | export function unregister() { 106 | if ("serviceWorker" in navigator) { 107 | navigator.serviceWorker.ready.then(registration => { 108 | registration.unregister(); 109 | }); 110 | } 111 | } 112 | --------------------------------------------------------------------------------