├── .env ├── .gitignore ├── README.md ├── now.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── screenshot.jpg ├── src ├── App.js ├── constants.js ├── index.css ├── index.js ├── lib-code.js ├── serviceWorker.js └── util.js └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | cache-control.sdgluck.now.sh 4 | 5 |

6 | 7 | - Build & learn about the Cache-Control header and its directives 8 | - Generates example library code for setting headers, e.g. nginx, express, & others 9 | - Share header configurations via shareable URLs (e.g. [public + max-age 2 days](https://cache-control.sdgluck.now.sh/?s=W3sibmFtZSI6InB1YmxpYyIsImFyZyI6bnVsbCwidGltZSI6InNlY3MifSx7Im5hbWUiOiJtYXgtYWdlIiwiYXJnIjoiMiIsInRpbWUiOiJkYXlzIn1d)) 10 | - Header descriptions taken from [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) 11 | - Inspired by [Cache-Control for Civilians](https://csswizardry.com/2019/03/cache-control-for-civilians/) 12 | 13 | 14 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "cache-control", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": "build" } 9 | } 10 | ], 11 | "routes": [ 12 | { 13 | "src": "/static/(.*)", 14 | "headers": { "cache-control": "s-maxage=31536000,immutable" }, 15 | "dest": "/static/$1" 16 | }, 17 | { "src": "/favicon.ico", "dest": "/favicon.ico" }, 18 | { "src": "/asset-manifest.json", "dest": "/asset-manifest.json" }, 19 | { "src": "/manifest.json", "dest": "/manifest.json" }, 20 | { "src": "/precache-manifest.(.*)", "dest": "/precache-manifest.$1" }, 21 | { 22 | "src": "/service-worker.js", 23 | "headers": { "cache-control": "s-maxage=0" }, 24 | "dest": "/service-worker.js" 25 | }, 26 | { 27 | "src": "/(.*)", 28 | "headers": { "cache-control": "s-maxage=0" }, 29 | "dest": "/index.html" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-control", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "clipboard-copy": "^3.0.0", 7 | "normalize.css": "^8.0.1", 8 | "react": "^16.8.4", 9 | "react-dom": "^16.8.4", 10 | "react-scripts": "2.1.8", 11 | "react-syntax-highlighter": "^10.2.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject", 18 | "now-build": "react-scripts build" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdgluck/cache-control/ffe3c6a395c529980fa5dd278e9e653742dbbbb7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | Cache-Control Header Builder 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdgluck/cache-control/ffe3c6a395c529980fa5dd278e9e653742dbbbb7/screenshot.jpg -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-comment-textnodes */ 2 | import React, { useState } from "react"; 3 | import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs"; 4 | import copyToClipboard from "clipboard-copy"; 5 | import SyntaxHighlighter from "react-syntax-highlighter"; 6 | 7 | import { libraryCode, libraryComment, libraryLanguage } from "./lib-code"; 8 | import { Times, Directives, DirectiveDescriptions } from "./constants"; 9 | import { styles, readInDirectives, createHeaderArg } from "./util"; 10 | 11 | import "normalize.css/normalize.css"; 12 | import "./index.css"; 13 | 14 | const directivePriorities = Object.keys(Directives); 15 | 16 | let initialOpenDirectives = window.location.search.includes("s=") 17 | ? readInDirectives().map(d => d.name) 18 | : directivePriorities.slice(0, 4); 19 | 20 | if (!initialOpenDirectives.length) { 21 | initialOpenDirectives = directivePriorities.slice(0, 4); 22 | } 23 | 24 | function Fieldset({ 25 | title, 26 | api, 27 | timeFields = false, 28 | enables = [], 29 | disables = [] 30 | }) { 31 | const [open, setOpen] = useState(initialOpenDirectives.includes(title)); 32 | 33 | return ( 34 |
35 |
setOpen(!open)} 38 | title={`Click to ${ 39 | open ? "hide" : "show" 40 | } "${title}" directive options`} 41 | > 42 | {title} 43 | {open ? "–" : "+"} 44 |
45 |
46 |
47 |

{DirectiveDescriptions[title]}

48 | {enables.length ? ( 49 |

50 | Requires: {enables.map(d => d.name).join(", ")} 51 |

52 | ) : null} 53 | {disables.length ? ( 54 |

55 | Cannot be used with: {disables.join(", ")} 56 |

57 | ) : null} 58 |
59 |
60 | 76 | {timeFields ? ( 77 | api.updateDirectiveArg(evt, title)} 83 | onTimeChange={evt => api.updateDirectiveTime(evt, title)} 84 | time={api.getDirectiveTime(title)} 85 | /> 86 | ) : null} 87 |
88 |
89 |
90 | ); 91 | } 92 | 93 | function Directive({ directive }) { 94 | let arg = directive.arg; 95 | if (arg !== null) { 96 | if (directive.time === Times.days) { 97 | arg *= 86400; 98 | } else if (directive.time === Times.hours) { 99 | arg *= 3600; 100 | } 101 | } 102 | arg = arg !== null ? " " + arg.toString() : ""; 103 | return ` ${directive.name}${directive.arg !== null ? arg : ""}`; 104 | } 105 | 106 | function TimeInput({ name, value, time, onArgChange, onTimeChange, active }) { 107 | let step = 60; 108 | 109 | if (time !== Times.secs) { 110 | step = 1; 111 | } 112 | 113 | return ( 114 |
115 | 126 | 137 | 148 | 159 |
160 | ); 161 | } 162 | 163 | export default class App extends React.Component { 164 | state = { 165 | showLibraryCode: true, 166 | codeLibrary: "express", 167 | directives: [], 168 | popupMessage: null 169 | }; 170 | 171 | componentDidMount() { 172 | this.setState({ 173 | directives: readInDirectives({ 174 | onError: () => { 175 | this.showPopup("Bad share url :("); 176 | } 177 | }) 178 | }); 179 | } 180 | 181 | setDirectives(directives) { 182 | this.setState({ 183 | directives: directives.sort((a, b) => { 184 | return ( 185 | directivePriorities.indexOf(a.name) - 186 | directivePriorities.indexOf(b.name) 187 | ); 188 | }) 189 | }); 190 | } 191 | 192 | hasDirective(directive) { 193 | return !!this.state.directives.find(d => d.name === directive); 194 | } 195 | 196 | toggleDirective(name, arg = null, disables = [], enables = []) { 197 | const active = this.hasDirective(name); 198 | let directives = this.state.directives; 199 | if (active) { 200 | directives = directives.filter(d => d.name !== name); 201 | } else { 202 | directives = directives.filter(d => !disables.includes(d.name)); 203 | directives.push({ name, arg, time: Times.secs }); 204 | if (enables.length) { 205 | enables.forEach(d => { 206 | if (!this.hasDirective(d.name)) { 207 | directives.push(d); 208 | } 209 | }); 210 | } 211 | } 212 | this.setDirectives(directives); 213 | } 214 | 215 | updateDirectiveArg(evt, name) { 216 | const active = this.hasDirective(name); 217 | if (!active) { 218 | return; 219 | } 220 | 221 | const directives = this.state.directives; 222 | const directive = directives.find(d => d.name === name); 223 | directive.arg = evt.target.value; 224 | this.setDirectives(directives); 225 | } 226 | 227 | updateDirectiveTime(evt, name) { 228 | const active = this.hasDirective(name); 229 | if (!active) { 230 | return; 231 | } 232 | 233 | const directives = this.state.directives; 234 | const directive = directives.find(d => d.name === name); 235 | const newTime = evt.target.value; 236 | const oldTime = directive.time; 237 | const oldTimeArg = directive.arg; 238 | let timeArg = oldTimeArg; 239 | 240 | if (oldTime === Times.secs) { 241 | if (newTime === Times.days) { 242 | timeArg = oldTimeArg / 86400; 243 | } else if (newTime === Times.hours) { 244 | timeArg = oldTimeArg / 3600; 245 | } 246 | } else if (oldTime === Times.hours) { 247 | if (newTime === Times.secs) { 248 | timeArg = oldTimeArg * 3600; 249 | } else if (newTime === Times.days) { 250 | timeArg = oldTimeArg / 24; 251 | } 252 | } else if (oldTime === Times.days) { 253 | if (newTime === Times.secs) { 254 | timeArg = oldTimeArg * 86400; 255 | } else if (newTime === Times.hours) { 256 | timeArg = oldTimeArg * 24; 257 | } 258 | } 259 | 260 | directive.time = newTime; 261 | directive.arg = Math.max(0, Math.round(timeArg)); 262 | 263 | this.setDirectives(directives); 264 | } 265 | 266 | getDirectiveTime(name) { 267 | const directive = this.state.directives.find(d => d.name === name); 268 | if (!directive) { 269 | return false; 270 | } 271 | return directive.time; 272 | } 273 | 274 | getDirectiveArg(name) { 275 | const directive = this.state.directives.find(d => d.name === name); 276 | if (!directive) { 277 | return null; 278 | } 279 | return directive.arg || ""; 280 | } 281 | 282 | copyToClipboard() { 283 | copyToClipboard( 284 | "Cache-Control: " + 285 | createHeaderArg(this.state.directives, false, false, "") 286 | ); 287 | } 288 | 289 | showPopup(popupMessage) { 290 | if (this.popupTimeout) { 291 | clearTimeout(this.popupTimeout); 292 | } 293 | this.setState({ popupMessage }); 294 | this.popupTimeout = setTimeout(() => { 295 | this.setState({ popupMessage: null }); 296 | }, 2000); 297 | } 298 | 299 | render() { 300 | const domain = window.location.protocol + "//" + window.location.host; 301 | const shareSearch = `?s=${btoa(JSON.stringify(this.state.directives))}`; 302 | const shareUrl = `${domain}${shareSearch}`; 303 | 304 | return ( 305 |
306 |
309 | {this.state.popupMessage} 310 |
311 |
312 |
313 |

Cache-Control Header Builder

314 |
315 | 342 |
343 |
344 |
345 |
350 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
366 |
371 |
382 |
393 |
394 |
395 |
396 | 397 |
this.copyToClipboard()}> 398 |
399 |
400 |                 Cache-Control:{`\n`}
401 |                 {this.state.directives.length ? (
402 |                   this.state.directives.map((d, i) => {
403 |                     return (
404 |                       
405 |                         
406 |                         {this.state.directives.length > 1 &&
407 |                         i !== this.state.directives.length - 1
408 |                           ? ",\n"
409 |                           : ""}
410 |                       
411 |                     );
412 |                   })
413 |                 ) : (
414 |                   <>
415 |                     
416 |                       {"  "}// configure directives
417 |                       
418 | {" "}// using the panel 419 |
420 | {" "}// on the left 421 |
422 | 423 | {" "}// configure directives 424 |
425 | {" "}// using the panel below 426 |
427 | 428 | )} 429 |
430 |
431 |
432 |
433 |
436 | this.setState({ 437 | showLibraryCode: !this.state.showLibraryCode 438 | }) 439 | } 440 | > 441 | Library Code{" "} 442 | ({this.state.showLibraryCode ? "hide" : "show"}) 443 |
444 |
445 | 460 |
461 |
462 |
463 | 470 | {libraryCode[this.state.codeLibrary]( 471 | createHeaderArg( 472 | this.state.directives, 473 | false, 474 | true, 475 | libraryComment(this.state.codeLibrary) 476 | ) 477 | )} 478 | 479 |
480 |
481 |
482 |
483 | 517 |
518 | ); 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const Directives = { 2 | public: "public", 3 | private: "private", 4 | "no-cache": "no-cache", 5 | "no-store": "no-store", 6 | "max-age": "max-age", 7 | "s-maxage": "s-maxage", 8 | "only-if-cached": "only-if-cached", 9 | "max-stale": "max-stale", 10 | "min-fresh": "min-fresh", 11 | "stale-while-revalidate": "stale-while-revalidate", 12 | "stale-if-error": "stale-if-error", 13 | "must-revalidate": "must-revalidate", 14 | "proxy-revalidate": "proxy-revalidate", 15 | immutable: "immutable", 16 | "no-transform": "no-transform" 17 | }; 18 | 19 | export const DirectiveDescriptions = { 20 | [Directives.public]: 21 | "Indicates that the response may be cached by any cache, even if the response would normally be non-cacheable (e.g. if the response does not contain a max-age directive or the Expires header).", 22 | [Directives.private]: 23 | "Indicates that the response is intended for a single user and must not be stored by a shared cache. A private cache may store the response.", 24 | [Directives["max-age"]]: 25 | "Specifies the maximum amount of time a resource will be considered fresh. Contrary to Expires, this directive is relative to the time of the request.", 26 | [Directives["s-maxage"]]: 27 | "Takes precedence over max-age or the Expires header, but it only applies to shared caches (e.g., proxies) and is ignored by a private cache.", 28 | [Directives["no-store"]]: 29 | "The cache should not store anything about the client request or server response.", 30 | [Directives["no-cache"]]: 31 | "Forces caches to submit the request to the origin server for validation before releasing a cached copy.", 32 | [Directives["only-if-cached"]]: 33 | "Indicates to not retrieve new data. This being the case, the server wishes the client to obtain a response only once and then cache. From this moment the client should keep releasing a cached copy and avoid contacting the origin-server to see if a newer copy exists.", 34 | [Directives["must-revalidate"]]: 35 | "Indicates that once a resource has become stale (e.g. max-age has expired), a cache must not use the response to satisfy subsequent requests for this resource without successful validation on the origin server.", 36 | [Directives["proxy-revalidate"]]: 37 | "Same as must-revalidate, but it only applies to shared caches (e.g., proxies) and is ignored by a private cache.", 38 | [Directives["immutable"]]: 39 | "Indicates that the response body will not change over time. The resource, if unexpired, is unchanged on the server and therefore the client should not send a conditional revalidation for it (e.g. If-None-Match or If-Modified-Since) to check for updates, even when the user explicitly refreshes the page.", 40 | [Directives["stale-while-revalidate"]]: 41 | "Indicates that the client is willing to accept a stale response while asynchronously checking in the background for a fresh one. The seconds value indicates for how long the client is willing to accept a stale response.", 42 | [Directives["stale-if-error"]]: 43 | "Indicates that the client is willing to accept a stale response if the check for a fresh one fails. The seconds value indicates for how long the client is willing to accept the stale response after the initial expiration.", 44 | [Directives["no-transform"]]: 45 | "No transformations or conversions should be made to the resource. The Content-Encoding, Content-Range, Content-Type headers must not be modified by a proxy. A non- transparent proxy might, for example, convert between image formats in order to save cache space or to reduce the amount of traffic on a slow link. The no-transform directive disallows this.", 46 | [Directives["min-fresh"]]: 47 | "Indicates that the client wants a response that will still be fresh for at least the specified number of seconds.", 48 | [Directives["max-stale"]]: 49 | "Indicates that the client is willing to accept a response that has exceeded its expiration time. Optionally, you can assign a value in seconds, indicating the time the response must not be expired by." 50 | }; 51 | 52 | export const Times = { 53 | secs: "secs", 54 | hours: "hours", 55 | days: "days" 56 | }; 57 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --colour-grey: #383838; 3 | } 4 | 5 | html, 6 | body, 7 | #root { 8 | width: 100%; 9 | height: 100%; 10 | font-family: sans-serif; 11 | color: var(--colour-grey); 12 | font-size: 12px; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | } 18 | 19 | a, 20 | a:active, 21 | a:visited, 22 | .Anchor { 23 | background-color: transparent; 24 | border: 0; 25 | padding: 0; 26 | margin: 0; 27 | cursor: pointer; 28 | color: white; 29 | } 30 | 31 | a, 32 | .Anchor { 33 | text-decoration: none; 34 | border-bottom: 1px dotted white; 35 | } 36 | 37 | a:hover, 38 | .Anchor:hover { 39 | text-decoration: none; 40 | border-bottom: 1px solid white; 41 | } 42 | 43 | .App { 44 | width: 100%; 45 | height: 100%; 46 | min-height: 100%; 47 | max-height: 100%; 48 | display: grid; 49 | grid-template-rows: 40px calc(100% - 80px) 40px; 50 | grid-template-columns: 1fr 2fr; 51 | grid-template-areas: 52 | "header header" 53 | "main main" 54 | "footer footer"; 55 | } 56 | 57 | .Header { 58 | width: 100%; 59 | height: 40px; 60 | background-color: var(--colour-grey); 61 | grid-area: header; 62 | padding: 5px 8px; 63 | color: pink; 64 | display: flex; 65 | justify-content: space-between; 66 | align-items: center; 67 | } 68 | 69 | .Header__heading { 70 | margin: 0; 71 | font-size: 1.5rem; 72 | } 73 | 74 | .Menu { 75 | list-style-type: none; 76 | margin: 0; 77 | padding: 0; 78 | } 79 | 80 | .Menu__item { 81 | float: left; 82 | } 83 | 84 | .Menu__item:not(:last-child) { 85 | padding-right: 10px; 86 | margin-right: 10px; 87 | border-right: 1px solid white; 88 | } 89 | 90 | .Main { 91 | grid-area: main; 92 | display: grid; 93 | grid-template-columns: minmax(400px, 1fr) 2fr; 94 | grid-template-areas: "config result"; 95 | } 96 | 97 | @media screen and (max-width: 720px) { 98 | .Main { 99 | grid-template-rows: auto 3fr; 100 | grid-template-columns: 1fr; 101 | grid-template-areas: "result" "config"; 102 | } 103 | 104 | .Result { 105 | border-bottom: 1px solid grey; 106 | } 107 | 108 | .ResultCode { 109 | display: none; 110 | } 111 | 112 | body .ResultHeader { 113 | margin-top: 0; 114 | } 115 | 116 | .Fieldset { 117 | font-size: 1.25rem; 118 | } 119 | 120 | .Fieldset__header { 121 | height: 40px; 122 | } 123 | 124 | body .ResultHeader__placeholder--mobile { 125 | display: block; 126 | } 127 | 128 | body .ResultHeader__placeholder--desktop { 129 | display: none; 130 | } 131 | } 132 | 133 | .Config { 134 | grid-area: config; 135 | overflow: auto; 136 | padding: 5px; 137 | position: relative; 138 | } 139 | 140 | .Fieldset:not(:last-child) { 141 | margin-bottom: 5px; 142 | } 143 | 144 | .Fieldset__header { 145 | cursor: pointer; 146 | color: pink; 147 | font-weight: bold; 148 | background-color: var(--colour-grey); 149 | padding: 5px 10px; 150 | border-radius: 5px; 151 | z-index: 1; 152 | position: relative; 153 | border-bottom: 1px solid white; 154 | display: flex; 155 | align-items: center; 156 | justify-content: space-between; 157 | } 158 | 159 | .Fieldset__body { 160 | padding: 5px 10px; 161 | padding-top: 10px; 162 | margin-top: -10px; 163 | border-radius: 5px; 164 | background-color: lightgrey; 165 | z-index: 0; 166 | border-bottom: 1px solid darkgrey; 167 | } 168 | 169 | .Fieldset__description { 170 | border-bottom: 1px solid darkgrey; 171 | } 172 | 173 | .Fieldset__fields { 174 | height: 40px; 175 | border-top: 1px solid ghostwhite; 176 | padding: 10px 0 5px 0; 177 | display: flex; 178 | flex-direction: row; 179 | justify-content: space-between; 180 | } 181 | 182 | .Radio { 183 | position: fixed; 184 | top: -999px; 185 | left: -999px; 186 | } 187 | 188 | label { 189 | display: flex; 190 | flex-direction: row; 191 | align-items: center; 192 | } 193 | 194 | input[type="number"] { 195 | height: 25px; 196 | width: 100px; 197 | border-top-left-radius: 5px; 198 | border-bottom-left-radius: 5px; 199 | border: 1px solid var(--colour-grey); 200 | padding-left: 5px; 201 | } 202 | 203 | input[type="checkbox"] { 204 | height: 20px; 205 | width: 20px; 206 | margin-right: 5px; 207 | } 208 | 209 | input[type="radio"] ~ span { 210 | display: inline-block; 211 | height: 25px; 212 | display: flex; 213 | align-items: center; 214 | padding: 0 5px; 215 | background-color: var(--colour-grey); 216 | color: white; 217 | cursor: pointer; 218 | } 219 | 220 | input[type="radio"] ~ span:hover { 221 | color: salmon; 222 | } 223 | 224 | input[type="radio"]:checked ~ span { 225 | color: pink; 226 | } 227 | 228 | .NumberRadioInput { 229 | display: flex; 230 | flex-direction: row; 231 | } 232 | 233 | .NumberRadio__radio--first span { 234 | padding-left: 4px; 235 | } 236 | 237 | .NumberRadio__radio--last span { 238 | padding-right: 4px; 239 | border-top-right-radius: 5px; 240 | border-bottom-right-radius: 5px; 241 | } 242 | 243 | .Result { 244 | grid-area: result; 245 | display: grid; 246 | grid-template-rows: 2fr auto; 247 | grid-template-areas: "result_header" "result_code"; 248 | background-color: lightgrey; 249 | } 250 | 251 | .ResultHeader { 252 | padding: 5px; 253 | grid-area: result_header; 254 | margin-top: -40px; 255 | color: #f66d85; 256 | font-size: 3rem; 257 | align-items: center; 258 | justify-content: center; 259 | display: flex; 260 | } 261 | 262 | .ResultHeader__placeholder--mobile { 263 | display: none; 264 | } 265 | 266 | .ResultHeader__placeholder--desktop { 267 | display: block; 268 | } 269 | 270 | @media screen and (max-width: 1200px) { 271 | .ResultHeader { 272 | font-size: 2rem; 273 | } 274 | } 275 | 276 | @media screen and (max-width: 900px) { 277 | .ResultHeader { 278 | font-size: 1.25rem; 279 | } 280 | } 281 | 282 | .ResultHeader__placeholder { 283 | color: grey; 284 | } 285 | 286 | .HeaderDirective { 287 | float: left; 288 | clear: both; 289 | } 290 | 291 | .HeaderDirective:not(:last-child):after { 292 | content: ","; 293 | } 294 | 295 | .ResultCode { 296 | grid-area: result_code; 297 | } 298 | 299 | .ResultCode__header { 300 | height: 40px; 301 | background-color: orange; 302 | color: white; 303 | width: 100%; 304 | display: flex; 305 | align-items: center; 306 | padding: 0 10px; 307 | font-weight: bold; 308 | justify-content: space-between; 309 | } 310 | 311 | .ResultCode__header-toggle { 312 | cursor: pointer; 313 | } 314 | 315 | .ResultCode__header-toggle span { 316 | border-bottom: 1px dotted white; 317 | } 318 | 319 | .ResultCode__header-select { 320 | border: 0; 321 | border-radius: 5px; 322 | padding: 3px 5px; 323 | } 324 | 325 | .ResultCode__code { 326 | font-size: 1.25rem; 327 | background-color: white; 328 | margin: 0; 329 | } 330 | 331 | .Footer { 332 | display: flex; 333 | align-items: center; 334 | justify-content: space-between; 335 | grid-area: footer; 336 | padding: 5px 8px; 337 | background-color: var(--colour-grey); 338 | color: white; 339 | } 340 | 341 | .Footer__resources span { 342 | display: inline-block; 343 | margin-right: 5px; 344 | } 345 | 346 | .Popup { 347 | position: fixed; 348 | top: 45px; 349 | right: 5px; 350 | background-color: black; 351 | color: white; 352 | padding: 5px; 353 | pointer-events: none; 354 | opacity: 0; 355 | } 356 | 357 | .Popup--show { 358 | opacity: 1; 359 | } 360 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 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(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/lib-code.js: -------------------------------------------------------------------------------- 1 | export const libraryLanguage = lang => 2 | ({ 3 | apache: "apache", 4 | nginx: "nginx" 5 | }[lang] || "javascript"); 6 | 7 | export const libraryComment = lang => 8 | ({ 9 | // apache doesn't allow comments on same line as directive? 10 | apache: "", 11 | nginx: "# no directives configured" 12 | }[lang] || "/* no directives configured */"); 13 | 14 | export const libraryCode = { 15 | express: directives => `\ 16 | // for all responses 17 | app.use((req, res, next) => { 18 | res.set('Cache-Control', ${directives}); 19 | next(); 20 | }); 21 | 22 | // for single response 23 | res.set('Cache-Control', ${directives});`, 24 | 25 | koa: directives => `\ 26 | // for all responses 27 | app.use(async (ctx, next) => { 28 | ctx.set('Cache-Control', ${directives}); 29 | await next(); 30 | }); 31 | 32 | // for single response 33 | ctx.set('Cache-Control', ${directives});`, 34 | 35 | hapi: directives => `\ 36 | // for all responses 37 | server.ext('onPreResponse', (request, reply) => { 38 | request.response.header('Cache-Control', ${directives}); 39 | reply(); 40 | }); 41 | 42 | // for single response 43 | response.header('Cache-Control', ${directives});`, 44 | 45 | "hapi v17": directives => `\ 46 | // for all responses 47 | server.route({ 48 | method: 'GET', 49 | path: '*', 50 | handler: (request, h) => { 51 | const response = h.response(); 52 | response.code(200); 53 | response.header('Cache-Control', ${directives}); 54 | return response; 55 | } 56 | }); 57 | 58 | // for single response 59 | response.header('Cache-Control', ${directives});`, 60 | 61 | fastify: directives => `\ 62 | // for all responses 63 | fastify.use('*', (request, reply, next) => { 64 | reply.header('Cache-Control', ${directives}); 65 | next(); 66 | }); 67 | 68 | // for single reponse 69 | reply.header('Cache-Control', ${directives})`, 70 | 71 | nginx: directives => `\ 72 | # for all responses 73 | location / { 74 | add_header 'Cache-Control' ${directives}; 75 | } 76 | 77 | # for single response 78 | add_header 'Cache-Control' ${directives};`, 79 | 80 | apache: directives => `\ 81 | # for all responses 82 | 83 | Header set "Cache-Control" ${directives} 84 | 85 | 86 | # for single response 87 | Header set "Cache-Control" ${directives}` 88 | }; 89 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 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.1/8 is 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 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import { Times } from "./constants"; 2 | 3 | export const styles = { 4 | hide: hide => (hide ? { display: "none" } : {}) 5 | }; 6 | 7 | export function createHeaderArg( 8 | directives, 9 | newlines = false, 10 | quotes = true, 11 | fallback = "" 12 | ) { 13 | let headerArg = ""; 14 | for (let i = 0; i < directives.length; i++) { 15 | const directive = directives[i]; 16 | let arg = directive.arg; 17 | if (arg !== null) { 18 | if (directive.time === Times.days) { 19 | arg *= 86400; 20 | } else if (directive.time === Times.hours) { 21 | arg *= 3600; 22 | } 23 | } 24 | headerArg += directive.name + (arg !== null ? " " + arg.toString() : ""); 25 | if (directives.length > 1 && i !== directives.length - 1) { 26 | headerArg = headerArg + "," + (newlines ? "\n" : " "); 27 | } 28 | } 29 | return headerArg.length ? (quotes ? `'${headerArg}'` : headerArg) : fallback; 30 | } 31 | 32 | export function readInDirectives({ onError = () => {} } = {}) { 33 | try { 34 | const parts = window.location.search.replace(/^\?/, "").split("&"); 35 | const params = parts.reduce((params, part) => { 36 | const [key, val] = part.split("="); 37 | return { ...params, [key]: val }; 38 | }, {}); 39 | return params.s ? JSON.parse(atob(params.s)) : []; 40 | } catch (err) { 41 | onError(err); 42 | return []; 43 | } 44 | } 45 | --------------------------------------------------------------------------------