├── .eleventy.js ├── .eleventyignore ├── .gitignore ├── 404.md ├── LICENSE ├── README.md ├── _data └── metadata.json ├── _includes ├── assets │ ├── css │ │ └── inline.css │ ├── js │ │ └── inline.js │ └── svg │ │ └── logo.svg ├── components │ ├── app.html │ ├── body-close.html │ ├── devs.njk │ ├── footer.html │ ├── form.njk │ ├── head.njk │ ├── meta.njk │ └── nav.njk ├── layouts │ ├── base.njk │ ├── contact.njk │ ├── dev.njk │ ├── devs.njk │ ├── home.njk │ ├── offline.njk │ ├── page.njk │ └── submitted.njk └── macros │ ├── form.njk │ ├── site.njk │ └── ui.njk ├── admin ├── config.yml ├── index.html └── preview-templates │ ├── index.js │ ├── page.js │ └── post.js ├── devs ├── devs.json └── tagged.njk ├── manifest.json ├── netlify.toml ├── package.json ├── pages ├── about.md ├── contact.md ├── home.md ├── offline.md ├── pages.json ├── submitted.md ├── thanks.md └── wants.md ├── static ├── img │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── droid.svg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── flying-car.svg │ ├── hologram-dash.svg │ ├── hologram-folder.svg │ ├── hologram.svg │ ├── icon.png │ ├── icon.svg │ ├── jetpack.svg │ ├── logo.svg │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ ├── ms-icon-70x70.png │ ├── netlify.svg │ ├── og-icon.png │ ├── repeating-pattern.png │ ├── repeating-pattern.svg │ └── sonic-screwdriver.svg └── js │ ├── indieconfig.js │ ├── sharing.js │ └── webaction.js └── sw.js /.eleventy.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require("luxon"); 2 | const CleanCSS = require("clean-css"); 3 | const UglifyJS = require("uglify-es"); 4 | const sanitizeHTML = require('sanitize-html') 5 | const htmlmin = require("html-minifier"); 6 | const widont = require("widont"); 7 | const md = require("markdown-it")({ 8 | html: true, 9 | linkify: true, 10 | typographer: true, 11 | }); 12 | const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); 13 | const inclusiveLangPlugin = require("@11ty/eleventy-plugin-inclusive-language"); 14 | 15 | module.exports = function(eleventyConfig) { 16 | 17 | // Date formatting (human readable) 18 | eleventyConfig.addFilter("readableDate", date => { 19 | return DateTime.fromJSDate(date).toFormat("dd LLL yyyy"); 20 | }); 21 | 22 | // Date formatting (machine readable) 23 | eleventyConfig.addFilter("machineDate", date => { 24 | return DateTime.fromJSDate(date).toISO(); 25 | }); 26 | 27 | // Markdownify 28 | eleventyConfig.addFilter("markdownify", text => { 29 | return md.renderInline( text ); 30 | }); 31 | 32 | // unSluggify 33 | eleventyConfig.addFilter("unslug", text => { 34 | text = text.charAt(0).toUpperCase() + text.slice(1); 35 | return text.replace(/-/g, ' '); 36 | }); 37 | 38 | // Name List 39 | eleventyConfig.addShortcode("NameList", names => { 40 | let strings = [], 41 | string = "", 42 | count = names.length; 43 | while ( count-- ) 44 | { 45 | let url = names[count].url || `https://twitter.com/${names[count].twitter}/`; 46 | strings.unshift( `${names[count].name}` ); 47 | } 48 | count = strings.length; 49 | if ( count > 2 ) 50 | { 51 | strings[count-1] = "and " + strings[count-1]; 52 | string = strings.join(", "); 53 | } 54 | else if ( count == 2 ) 55 | { 56 | string = `${strings[0]} and ${strings[1]}`; 57 | } 58 | else 59 | { 60 | string = strings[0]; 61 | } 62 | return `${string}`; 63 | }); 64 | 65 | // Fix proper nouns 66 | eleventyConfig.addFilter("fixNames", text => { 67 | let test = text.toLowerCase(), 68 | acronyms = [ "html", "css", "svg" ], 69 | camel_case = [ "JavaScript" ], 70 | i, proper_name; 71 | 72 | if ( acronyms.indexOf( test ) > -1 ) 73 | { 74 | return text.toUpperCase(); 75 | } 76 | else 77 | { 78 | for ( i in camel_case ) 79 | { 80 | proper_name = camel_case[i]; 81 | if ( proper_name.toLowerCase() == test ) 82 | { 83 | return proper_name; 84 | } 85 | } 86 | } 87 | return text; 88 | }); 89 | 90 | // Widont 91 | eleventyConfig.addFilter("widont", function(text) { 92 | return `${widont( text )}`; 93 | }); 94 | 95 | 96 | // Minify CSS 97 | eleventyConfig.addFilter("cssmin", function(code) { 98 | return new CleanCSS({}).minify(code).styles; 99 | }); 100 | 101 | // Minify JS 102 | eleventyConfig.addFilter("jsmin", function(code) { 103 | let minified = UglifyJS.minify(code); 104 | if (minified.error) { 105 | console.log("UglifyJS error: ", minified.error); 106 | return code; 107 | } 108 | return minified.code; 109 | }); 110 | 111 | // Minify HTML output 112 | eleventyConfig.addTransform("htmlmin", function(content, outputPath) { 113 | if (outputPath.indexOf(".html") > -1) { 114 | let minified = htmlmin.minify(content, { 115 | useShortDoctype: true, 116 | removeComments: true, 117 | collapseWhitespace: true, 118 | collapseBooleanAttributes: true 119 | }); 120 | minified = minified.replace('\u00a0', '\u00a0'); 121 | return minified; 122 | } 123 | return content; 124 | }); 125 | 126 | // Minify JS output 127 | eleventyConfig.addTransform("jsmin", function(content, outputPath) { 128 | if (outputPath.indexOf(".js") > -1) { 129 | let minified = UglifyJS.minify(content); 130 | return minified; 131 | } 132 | return content; 133 | }); 134 | 135 | // limit filter 136 | eleventyConfig.addNunjucksFilter("limit", function(array, limit) { 137 | return array.slice(0, limit); 138 | }); 139 | 140 | eleventyConfig.addCollection("devs", collection => { 141 | // get unsorted items 142 | return collection.getAll().filter( item => { 143 | return item.inputPath.indexOf("devs/") > -1; 144 | }); 145 | }); 146 | eleventyConfig.addFilter("extractID", url => { 147 | url = url.split("/"); 148 | return url[2]; 149 | }); 150 | 151 | eleventyConfig.addCollection("tags", function(collection) { 152 | // get unsorted items 153 | var tags = []; 154 | collection.getAll() 155 | .map( item => { 156 | if ( item.inputPath.indexOf("wants/") > -1 ) 157 | { 158 | item.data.tags.map( tag => { 159 | if ( tags.indexOf( tag ) < 0 ) 160 | { 161 | tags.push( tag ); 162 | } 163 | }); 164 | } 165 | }); 166 | return tags.sort(); 167 | }); 168 | 169 | eleventyConfig.addFilter("toString", function(collection, separator, props) { 170 | var ret = [], 171 | i = collection.length; 172 | while ( i-- ) 173 | { 174 | let str = [], 175 | j = props.length; 176 | while ( j-- ) 177 | { 178 | let text = collection[i].data[props[j]]; 179 | if ( props[j].indexOf("date") > -1 ) 180 | { 181 | text = new Date( text ); 182 | text = DateTime.fromJSDate(text).toFormat("dd LLL yyyy"); 183 | } 184 | str.unshift( text ); 185 | } 186 | ret.unshift( str.join( separator ) ); 187 | } 188 | return ret; 189 | }); 190 | 191 | eleventyConfig.addFilter("getDirectory", function(url) { 192 | url = url.split('/'); 193 | return url[1]; 194 | }); 195 | 196 | // Don't process folders with static assets e.g. images 197 | eleventyConfig.addPassthroughCopy("sw.js"); 198 | eleventyConfig.addPassthroughCopy("static/img"); 199 | eleventyConfig.addPassthroughCopy("static/js"); 200 | eleventyConfig.addPassthroughCopy("manifest.json"); 201 | eleventyConfig.addPassthroughCopy("admin"); 202 | // eleventyConfig.addPassthroughCopy("_includes/assets/"); 203 | 204 | /* Markdown Plugins */ 205 | let markdownIt = require("markdown-it"); 206 | let markdownItAnchor = require("markdown-it-anchor"); 207 | let options = { 208 | html: true, 209 | breaks: true, 210 | linkify: true 211 | }; 212 | let opts = { 213 | permalink: false 214 | }; 215 | 216 | eleventyConfig.setLibrary("md", markdownIt(options) 217 | .use(markdownItAnchor, opts) 218 | ); 219 | 220 | eleventyConfig.addPlugin(syntaxHighlight); 221 | 222 | //eleventyConfig.addPlugin(inclusiveLangPlugin); 223 | 224 | return { 225 | templateFormats: ["md", "njk", "html", "liquid"], 226 | 227 | // If your site lives in a different subdirectory, change this. 228 | // Leading or trailing slashes are all normalized away, so don’t worry about it. 229 | // If you don’t have a subdirectory, use "" or "/" (they do the same thing) 230 | // This is only used for URLs (it does not affect your file structure) 231 | pathPrefix: "/", 232 | 233 | markdownTemplateEngine: "liquid", 234 | htmlTemplateEngine: "njk", 235 | dataTemplateEngine: "njk", 236 | passthroughFileCopy: true, 237 | dir: { 238 | input: ".", 239 | includes: "_includes", 240 | data: "_data", 241 | output: "_site" 242 | } 243 | }; 244 | }; 245 | -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | node_modules/ 3 | package-lock.json 4 | .env -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 404 3 | layout: layouts/page.njk 4 | permalink: /404.html 5 | --- 6 | Uh oh, the resource you requested isn't here. Sorry. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aaron Gustafson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devsofcolour 2 | A searchable and sortable database to find devs of colour of all skill levels, languages, frameworks. 3 | 4 | # How to build locally 5 | to build locally: 6 | * first make sure you have [node](https://nodejs.org) installed (this has been tested with node version v10.15.3 and npm version 6.4.1) 7 | * While in the `devsofcolour` directory in your terminal of choice, run `npm install` to install the repos packaged dependencies 8 | * once that's complete, run `npm start` 9 | * your terminal prompt should provide you with a few different local addresses to access both the live build of the website as well as the CMS interface for managing content 10 | * If you run into any issues or have concerns please use the [issues tab](https://github.com/tatianamac/devsofcolour/issues) on the github repo. -------------------------------------------------------------------------------- /_data/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Devs of Colour", 3 | "url": "https://devsofcolour.org", 4 | "domain": "devsofcolour.org", 5 | "author": { 6 | "name": "Tatiana Mac", 7 | "email": "??", 8 | "github": "https://github.com/tatianamac" 9 | } 10 | } -------------------------------------------------------------------------------- /_includes/assets/css/inline.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Devs of Colour 4 | 5 | colors: 6 | - Green: #3b9559 7 | - Dark Green: #005e22 8 | - Mint: #cef6da; 9 | - White: #fff 10 | - Yellow: #ffffeb 11 | - Black: #003d16 12 | 13 | */ 14 | 15 | *, *:before, *:after { 16 | box-sizing: border-box; 17 | transition: color .5s, background-color .5s; 18 | } 19 | 20 | html, 21 | body { 22 | color: #003d16; 23 | 24 | font-family: Poppins, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; 25 | font-size: 1em; 26 | 27 | font-smooth: always; 28 | -webkit-font-smoothing: subpixel-antialiased; 29 | -moz-osx-font-smoothing: grayscale; 30 | 31 | line-height: 1.5; 32 | 33 | padding: 0; 34 | margin: 0; 35 | 36 | display: flex; 37 | min-height: 100vh; 38 | flex-direction: column; 39 | } 40 | 41 | h1, h2, h3, h4, h5, h6 { 42 | font-weight: 400; 43 | margin: 0; 44 | } 45 | h1, h2 { 46 | line-height: 1.1; 47 | } 48 | h3, h4 { 49 | line-height: 1.3; 50 | } 51 | h5 { 52 | line-height: 1.4; 53 | } 54 | h1 { font-size: 3.75rem; } 55 | h2 { font-size: 2.875rem; } 56 | h3 { font-size: 2.125rem; } 57 | h4 { font-size: 1.75rem; } 58 | h5 { font-size: 1.25rem; } 59 | h6 { font-size: 1rem; } 60 | 61 | p, 62 | pre, 63 | code { 64 | line-height: 1.5; 65 | } 66 | 67 | code { 68 | background: #CFF7D9; 69 | padding: 0 .25em; 70 | border: 1px solid #3D9558; 71 | border-radius: .15em; 72 | font-size: 1.25em; 73 | color: #005e22; 74 | } 75 | 76 | a[href], 77 | a[href]:visited { 78 | color: #005e22; 79 | } 80 | 81 | main { 82 | background: rgba(255, 255, 255, 0.8); 83 | padding: 2em 2em; 84 | margin: 0 auto; 85 | flex: 1; 86 | width: 100%; 87 | } 88 | @media screen and (min-width: 80ch) { 89 | main { 90 | max-width: 80ch; 91 | } 92 | } 93 | 94 | main * + * { 95 | margin-top: 1rem; 96 | } 97 | 98 | main :first-child, 99 | main > article :first-child { 100 | margin-top: 0; 101 | } 102 | 103 | pre { 104 | font-size: 14px; 105 | direction: ltr; 106 | text-align: left; 107 | white-space: pre-wrap; 108 | } 109 | 110 | pre code { 111 | display: block; 112 | padding: .5em 1em; 113 | } 114 | 115 | small { 116 | display: block; 117 | font-size: .875rem; 118 | line-height: 1.25; 119 | margin-top: .5rem; 120 | } 121 | 122 | /* Header */ 123 | header { 124 | margin: 0 auto; 125 | width: 100%; 126 | max-width: 80ch; 127 | } 128 | #logo { 129 | display: block; 130 | padding: 2em 1em 0; 131 | margin: 0 auto; 132 | width: 100%; 133 | max-width: 555px; /* logo width */ 134 | } 135 | .logo-container { 136 | display: block; 137 | margin: 0 auto; 138 | width: 100%; 139 | height: 0; 140 | padding: 35.675675% 0 0; 141 | font-weight: bold; 142 | font-style: normal; 143 | text-decoration: none; 144 | position: relative; 145 | } 146 | .logo-container svg { 147 | display: block; 148 | width: 100%; 149 | height: auto; 150 | position: absolute; 151 | top: 0; 152 | left: 0; 153 | bottom: 0; 154 | right: 0; 155 | } 156 | 157 | 158 | 159 | /* Header Nav */ 160 | nav ul { 161 | font-size: 1.5rem; 162 | list-style: none; 163 | margin: 2rem auto 0; 164 | padding: 0; 165 | text-align: center; 166 | } 167 | nav a { 168 | display: block; 169 | padding: .25em 1em 1px; 170 | margin: 0; 171 | 172 | font-weight: 600; 173 | text-decoration: none; 174 | text-transform: uppercase; 175 | letter-spacing: 0.02em; 176 | } 177 | [aria-current] { 178 | background: rgba(255, 255, 255, 0.8); 179 | } 180 | @media screen and (min-width: 36em) { 181 | nav ul { 182 | display: flex; 183 | justify-content: center; 184 | } 185 | nav li { 186 | position: relative; 187 | } 188 | nav li::before, 189 | nav li::after, 190 | nav a::before { 191 | background: rgba(255, 255, 255, 0); 192 | content: ""; 193 | position: absolute; 194 | animation-fill-mode: forwards; 195 | } 196 | nav li:hover::before, 197 | nav li:hover::after, 198 | nav a:hover::before { 199 | background: rgba(255, 255, 255, 1); 200 | } 201 | nav li::before { 202 | bottom: 0; 203 | left: 0; 204 | width: 1px; 205 | height: 0; 206 | transition: height .25s; 207 | } 208 | nav li::after { 209 | top: 0; 210 | left: 0; 211 | width: 0; 212 | height: 1px; 213 | transition: width .5s .25s; 214 | } 215 | nav li a::before { 216 | top: 1px; 217 | right: 0; 218 | width: 1px; 219 | height: 0; 220 | transition: height .25s .75s; 221 | } 222 | nav li:hover::before { 223 | height: calc( 100% - 1px ); 224 | } 225 | nav li:hover::after { 226 | width: 100%; 227 | } 228 | nav a:hover::before { 229 | height: calc( 100% - 1px ); 230 | } 231 | } 232 | 233 | /* sharing */ 234 | .share dt { 235 | font-weight: bold; 236 | } 237 | .share dd { 238 | margin: .25em 0 0; 239 | padding: 0; 240 | } 241 | .share-link { 242 | border-radius: 6px; 243 | display: inline-block; 244 | margin: 0 .25em; 245 | padding: 4px 8px; 246 | transition: filter .5s; 247 | } 248 | .share-link:hover, 249 | .share-link:focus, 250 | .share-link:active { 251 | filter: brightness(75%) contrast(2.5); 252 | } 253 | .share-link__icon { 254 | vertical-align: top; 255 | display: inline-block; 256 | } 257 | .share-link__icon path { 258 | fill: white; 259 | } 260 | .share-link__text { 261 | position: absolute !important; 262 | height: 1px; 263 | width: 1px; 264 | overflow: hidden; 265 | clip: rect(1px, 1px, 1px, 1px); 266 | } 267 | .share-link--twitter { 268 | background: #26c4f1; 269 | } 270 | .share-link--twitter .share-link__icon { 271 | width: 22px; 272 | height: 22px; 273 | margin-top: 3px; 274 | } 275 | .share-link--linkedin { 276 | background: #007bb6; 277 | } 278 | .share-link--linkedin .share-link__icon { 279 | width: 23px; 280 | height: 23px; 281 | margin-top: 3px; 282 | } 283 | .share-link--facebook { 284 | background: #306199; 285 | } 286 | .share-link--facebook .share-link__icon { 287 | width: 20px; 288 | height: 20px; 289 | margin-top: 4px; 290 | } 291 | 292 | /* Tags & votes */ 293 | .tags > * { 294 | display: inline; 295 | margin: 0; 296 | padding: 0; 297 | } 298 | .tags dt { 299 | font-weight: bold; 300 | } 301 | .tags dt::after { 302 | content: ": "; 303 | } 304 | 305 | /* Footer */ 306 | footer { 307 | background: #005e22; 308 | color: #fff; 309 | } 310 | footer a[href], footer a[href]:visited { 311 | color: #fff; 312 | } 313 | footer p { 314 | max-width: 80ch; 315 | margin: 0 auto; 316 | padding: 2em; 317 | } 318 | footer small + small { 319 | margin-top: .75rem; 320 | } 321 | 322 | 323 | /* Sections */ 324 | section { 325 | margin-top: 3rem; 326 | } 327 | 328 | /* Post Tags */ 329 | a[rel="tag"], 330 | a[rel="tag"]:visited { 331 | display: inline-block; 332 | vertical-align: text-top; 333 | text-transform: uppercase; 334 | letter-spacing: .1em; 335 | font-size: .625em; 336 | padding: 0 .5em; 337 | line-height: 2em; 338 | height: 2em; 339 | border: 1px solid var(--white); 340 | background-color: var(--white); 341 | color: var(--blue); 342 | border-radius: .25em; 343 | text-decoration: none; 344 | margin: 0 .5em .5em 0; 345 | } 346 | 347 | a[rel="tag"]:hover { 348 | border: 1px solid var(--blue); 349 | background-color: var(--blue); 350 | color: var(--white); 351 | } 352 | 353 | a[rel="tag"]:last-child { 354 | margin-right: 0; 355 | } 356 | 357 | /* Pagination */ 358 | .pagination { 359 | margin-top: 3rem; 360 | padding-top: 1rem; 361 | border-top: 1px solid #005e22; 362 | 363 | display: grid; 364 | grid-template: "prev page next"; 365 | grid-gap: 1rem; 366 | align-items: center; 367 | grid-auto-columns: 1fr; 368 | } 369 | .pagination strong { 370 | grid-area: page; 371 | justify-self: center; 372 | } 373 | .pagination a { 374 | padding: 0; 375 | } 376 | .pagination a[rel=prev] { 377 | grid-area: prev; 378 | justify-self: start; 379 | } 380 | .pagination a[rel=next] { 381 | grid-area: next; 382 | justify-self: end; 383 | } 384 | 385 | /* Form */ 386 | .questions { 387 | margin: 0; 388 | padding: 0; 389 | list-style: none; 390 | perspective: 1000px; 391 | } 392 | .question { 393 | margin-bottom: 1.5rem; 394 | transition: transform .15s; 395 | transform-origin: 50% 50%; 396 | } 397 | .question:focus-within { 398 | transform: scale(1.02); 399 | } 400 | .question__addendum, 401 | .question__description { 402 | display: block; 403 | margin-top: .3rem; 404 | } 405 | .question__description { 406 | font-size: .875rem; 407 | line-height: 1.25; 408 | } 409 | label { 410 | display: block; 411 | font-weight: 600; 412 | } 413 | form br { 414 | display: none; 415 | } 416 | input, 417 | textarea, 418 | select { 419 | background-color: #fff; 420 | font-size: 1rem; 421 | font-family: inherit; 422 | border: 1px solid #003d16; 423 | margin-top: .15rem; 424 | padding: .25em .5em; 425 | width: 100%; 426 | } 427 | input:focus, 428 | select:focus, 429 | textarea:focus { 430 | background-color: #ffffeb; 431 | } 432 | label input { 433 | width: auto; 434 | } 435 | 436 | .question--confirm { 437 | font-weight: normal; 438 | } 439 | .question--confirm input { 440 | width: auto; 441 | vertical-align: middle; 442 | /* hacky */ 443 | position: relative; 444 | top: -.1em; 445 | } 446 | 447 | 448 | button, 449 | .button { 450 | cursor: pointer; 451 | display: inline-block; 452 | background-color: #005e22; 453 | color: #fff; 454 | border: 0; 455 | border-radius: .5rem; 456 | font-size: 1rem; 457 | font-family: inherit; 458 | padding: .5em 3em; 459 | transform: translateX(0) translateY(0); 460 | box-shadow: none; 461 | transition: transform .25s, box-shadow .25s, background-color .5s; 462 | } 463 | a[href].button, 464 | a[href].button:visited { 465 | color: #fff; 466 | text-decoration: none; 467 | margin-top: .25rem; 468 | padding: .5em 1em; 469 | } 470 | .button .button__icon { 471 | width: auto; 472 | height: 22px; 473 | transform: translate(-2px, 4px); 474 | } 475 | .button__icon path { 476 | fill: white; 477 | } 478 | button:hover, 479 | button:focus, 480 | .button:hover, 481 | .button:focus { 482 | background-color: #005e22; 483 | background-color: #005e22; 484 | transform: translateX(-2px) translateY(-2px); 485 | box-shadow: 3px 3px 0 #CFF7D9, 6px 6px 0 #3D9558; 486 | outline: 0; 487 | } 488 | button:hover:active, 489 | button:focus:active, 490 | .button:hover:active, 491 | .button:focus:active { 492 | transform: translateX(0) translateY(0); 493 | box-shadow: none; 494 | transition: background-color .5s; 495 | } 496 | 497 | @media screen and (min-width:768px) { 498 | :root { 499 | font-size: 1.1rem; 500 | } 501 | } 502 | 503 | .devs { 504 | list-style: none; 505 | padding: 0; 506 | margin-left: 0; 507 | } 508 | .dev + .dev { 509 | margin-top: 2rem; 510 | border-top: 1px solid #005e22; 511 | padding-top: 1rem; 512 | } 513 | .dev__meta { 514 | font-style: italic; 515 | font-size: .875rem; 516 | } 517 | .dev__title { 518 | font-size: 2.125rem; 519 | } 520 | h1.dev__title { 521 | font-size: 2.875rem; 522 | } 523 | 524 | 525 | [hidden] { 526 | display: none; 527 | } 528 | 529 | hr { 530 | background: #005e22; 531 | border: 0; 532 | margin-top: 2rem; 533 | height: 1px; 534 | } 535 | 536 | @media only screen and (max-width: 40em) { 537 | b.shy::after { 538 | content: " "; 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /_includes/assets/js/inline.js: -------------------------------------------------------------------------------- 1 | if (window.netlifyIdentity) { 2 | window.netlifyIdentity.on("init", user => { 3 | if (!user) { 4 | window.netlifyIdentity.on("login", () => { 5 | document.location.href = "/admin/"; 6 | }); 7 | } 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /_includes/assets/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /_includes/components/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /_includes/components/body-close.html: -------------------------------------------------------------------------------- 1 | {% if javascript.body %} 2 | {% for js in javascript.body %} 3 | 4 | {% endfor %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /_includes/components/devs.njk: -------------------------------------------------------------------------------- 1 | {% from "macros/site.njk" import dev %} 2 | 3 | 8 | -------------------------------------------------------------------------------- /_includes/components/footer.html: -------------------------------------------------------------------------------- 1 |

2 | Devs of Colour is run by a bunch of awesome people who ❤️ the web! 3 | This site’s contents are available under the Creative Commons Attribution-ShareAlike 4.0 International License. Its source code is freely shared under the MIT license. 4 | Edits welcome on GitHub. Hosted on Netlify. 5 | You can also follow us on Twitter for more. 6 |

7 | -------------------------------------------------------------------------------- /_includes/components/form.njk: -------------------------------------------------------------------------------- 1 | {% from "macros/form.njk" import label, field, confirm, select, textarea, button %} 2 | 3 |
4 |
    5 |
  1. 6 | {{ label("What’s your name?", "name") }} 7 | {{ field( "text", "name", { required: true, placeholder: "Katherine Johnson", autocomplete: "name", autocorrect: "off", autocapitalize: "off" } ) }} 8 |
    9 | {{ confirm("Please keep my full name private", "privacy") }} 10 |
    11 |
  2. 12 |
  3. 13 | {{ label("Where can we email you?", "email") }} 14 | {{ field( "email", "email", { required: true, placeholder: "katherine@johnson.tld", autocomplete: "email", description: "We will only use this information to contact you when your want is published and reach out if we select you to participate in an event. We will not retain your email address beyond that period of time." } ) }} 15 |
  4. 16 |
  5. 17 | {% set upcoming_events = collections.upcoming_events | toString( " - ", ["title", "location"]) %} 18 | {{ label("If you are already attending one of the events we’ll be at, would you consider presenting your idea in person?", "events") }} 19 | {{ select( "events", upcoming_events, { required: true, options_before: ["I’m not attending an event, but am open to my submission being shared at one", "I’m not interested in having my submission shared at an event"], description: "If you are not already planning to attend the event, it is unlikely we will be able to get you a ticket. That said, if you might qualify for an attendee scholarship, contact us and we will try to help you if we can." } ) }} 20 |
  6. 21 |
  7. 22 | {{ label("What do you want?", "title") }} 23 | {{ field( "text", "title", { required: true, placeholder: "I want…", autocapitalize: "sentences", spellcheck: "true" } ) }} 24 |
  8. 25 |
  9. 26 | {{ label("Now go into a little more detail. How is lack of this impacting your work? How do you currently work around this limitation?", "detail") }} 27 | {{ textarea( "detail", { required: true, autocapitalize: "sentences", spellcheck: "true", description: "Please note that we may end up rewriting your problem before posting it to this site." } ) }} 28 |
  10. 29 | 32 |
33 | {{ button("Send it in") }} 34 |

Note: You can ask us to remove your personal information from this site or our form submission database at any time. We can also retroactively remove your full name from any submissions. Just drop us a line.

35 |
36 | 37 | 38 | 43 | 44 | 103 | -------------------------------------------------------------------------------- /_includes/components/head.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "components/meta.njk" %} 5 | 6 | 7 | 8 | 9 | {% set css %} 10 | {% include "assets/css/inline.css" %} 11 | {% endset %} 12 | 13 | 14 | {# commenting out the netlify stuff 15 | 16 | 17 | {% set js %} 18 | {% include "assets/js/inline.js" %} 19 | {% endset %} 20 | 21 | #} 22 | 23 | {% include "components/app.html" %} 24 | 25 | 26 | 27 | 28 | 29 | {# do we want webmentions? 30 | 31 | 32 | #} 33 | 34 | {% if javascript.head %} 35 | {% for js in javascript.head %} 36 | 37 | {% endfor %} 38 | {% endif %} 39 | 40 | -------------------------------------------------------------------------------- /_includes/components/meta.njk: -------------------------------------------------------------------------------- 1 | {% set page_title %}{{ title or renderData.title or metadata.title }}{% endset %} 2 | {% set page_description %}{{ description or pkg.description }}{% endset %} 3 | {% set canonical %}{{ pkg.homepage }}{{ page.url }}{% endset %} 4 | {% set og_image %}{{ pkg.homepage }}/static/img/og-icon.png{% endset %} 5 | 6 | {{ page_title }}{% if page_title != pkg.title %} ↬ {{ pkg.title }}{% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 46 | -------------------------------------------------------------------------------- /_includes/components/nav.njk: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /_includes/layouts/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "components/head.njk" %} 5 | 6 | {% if body_class %} 7 | 8 | {% else %} 9 | 10 | {% endif %} 11 | 12 |
13 | 18 | {% include "components/nav.njk" %} 19 |
20 | 21 |
22 | {{ layoutContent | safe }} 23 |
24 | 25 | 28 | 29 | {% include "components/body-close.html" %} 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /_includes/layouts/contact.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: contact 4 | --- 5 |

{{ title | widont }}

6 | 7 | {{ layoutContent | safe }} 8 | 9 | {% from "macros/form.njk" import label, field, textarea, button %} 10 | 11 |
12 |
    13 |
  1. 14 | {{ label("How should we address you?", "name") }} 15 | {{ field( "text", "name", { required: true, placeholder: "Katherine Johnson", autocomplete: "name", autocorrect: "off", autocapitalize: "off" } ) }} 16 |
  2. 17 |
  3. 18 | {{ label("What’s your email address?", "email") }} 19 | {{ field( "email", "email", { required: true, placeholder: "katherine@johnson.tld", autocomplete: "email" } ) }} 20 |
  4. 21 |
  5. 22 | {{ label("What’s on your mind?", "message") }} 23 | {{ textarea( "message", { required: true, autocapitalize: "sentences", spellcheck: "true" } ) }} 24 |
  6. 25 | 28 |
29 | {{ button("Send in your message") }} 30 |
-------------------------------------------------------------------------------- /_includes/layouts/dev.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: devs 4 | body_class: "dev h-entry" 5 | javascript: 6 | head: 7 | - /static/js/indieconfig.js 8 | - /static/js/webaction.js 9 | body: 10 | - /static/js/sharing.js 11 | --- 12 | 13 | {% set absoluteUrl %}{{ metadata.url }}{{ page.url | url }}{% endset %} 14 | {% set id %}{{ page.url | extractID }}{% endset %} 15 | 16 |

{{ title | markdownify | widont | safe }}

17 | 18 |
19 | {{ layoutContent | safe }} 20 |
21 | 22 |
23 |
Do you want this too? Share it!
24 |
25 | 29 | 33 | 37 |
38 |
39 | 40 |
41 |
Tagged
42 |
43 | {% for tag in tags %} 44 | {{ tag | unslug | fixNames }} 45 | {% endfor %} 46 |
47 |
48 | 49 | -------------------------------------------------------------------------------- /_includes/layouts/devs.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: wants 4 | permalink: "/wants/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}{% else %}index{% endif %}.html" 5 | pagination: 6 | data: collections.devs 7 | size: 10 8 | alias: devs 9 | --- 10 | 11 | {% from "macros/ui.njk" import paginate %} 12 | 13 |

{{ title | widont }}

14 | 15 | {{ layoutContent | safe }} 16 | 17 | {% if devs.length > 0 %} 18 | {% include "components/devs.njk" %} 19 | {{ paginate( "Page", pagination ) }} 20 | {% else %} 21 |

No one has submitted a profile yet. Wanna be the first?

22 | {% endif %} 23 | -------------------------------------------------------------------------------- /_includes/layouts/home.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: home 4 | --- 5 | 6 | {{ layoutContent | safe }} 7 | -------------------------------------------------------------------------------- /_includes/layouts/offline.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: offline 4 | --- 5 | 6 |

{{ title | widont }}

7 | 8 | {{ layoutContent | safe }} 9 | 10 | 18 | 19 | 31 | 32 | -------------------------------------------------------------------------------- /_includes/layouts/page.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: page 4 | --- 5 | 6 |

{{ title | widont }}

7 | 8 | {{ layoutContent | safe }} 9 | -------------------------------------------------------------------------------- /_includes/layouts/submitted.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | --- 4 | 5 |

{{ title | widont }}

6 | 7 | {{ layoutContent | safe }} 8 | -------------------------------------------------------------------------------- /_includes/macros/form.njk: -------------------------------------------------------------------------------- 1 | {# =================== 2 | Forms 3 | =================== #} 4 | 5 | {% macro label( text, name ) %} 6 | 7 | {% endmacro %} 8 | 9 | {% macro field( type, name, data ) %} 10 |
11 | 24 | {% if data.description %} 25 |
26 | {{ data.description | widont }} 27 | {% endif %} 28 | {% endmacro %} 29 | 30 | {% macro confirm( text, name, data ) %} 31 | 42 | {% if data.description %} 43 |
44 | {{ data.description | widont }} 45 | {% endif %} 46 | {% endmacro %} 47 | 48 | {% macro select( name, options, data ) %} 49 |
50 | 66 | {% if data.description %} 67 |
68 | {{ data.description | widont }} 69 | {% endif %} 70 | {% endmacro %} 71 | 72 | {% macro textarea( name, data ) %} 73 |
74 | 84 | {% if data.description %} 85 | {{ data.description | widont }} 86 | {% endif %} 87 | {% endmacro %} 88 | 89 | {% macro radios( label, name, options ) %} 90 |
91 | {{ label }} 92 | 118 | {% if data.description %} 119 | {{ data.description | widont }} 120 | {% endif %} 121 |
122 | {% endmacro %} 123 | 124 | {% macro checkboxes( label, name, options ) %} 125 |
126 | {{ label }} 127 | 153 | {% if data.description %} 154 | {{ data.description | widont }} 155 | {% endif %} 156 |
157 | {% endmacro %} 158 | 159 | {% macro button( text ) %} 160 | 161 | {% endmacro %} 162 | 163 | 164 | 165 | {# =================== 166 | UI 167 | =================== #} 168 | {% macro paginate( what, pagination ) %} 169 | 174 | {% endmacro %} -------------------------------------------------------------------------------- /_includes/macros/site.njk: -------------------------------------------------------------------------------- 1 | {# ======================== 2 | Site-specific Macros 3 | ======================== #} 4 | 5 | {% macro dev( block_tag, additional_classes, url, title_tag, title, submitter ) %} 6 | <{{ block_tag }} class="want {{ additional_classes }}"> 7 | <{{ title_tag }}>{{ title | markdownify | widont | safe }} 8 |

Submitted by {{ submitter }}

9 | 10 | {% endmacro %} -------------------------------------------------------------------------------- /_includes/macros/ui.njk: -------------------------------------------------------------------------------- 1 | {# =================== 2 | UI 3 | =================== #} 4 | 5 | {% macro paginate( what, pagination ) %} 6 | 11 | {% endmacro %} -------------------------------------------------------------------------------- /admin/config.yml: -------------------------------------------------------------------------------- 1 | backend: 2 | name: github 3 | repo: WebWeWant/webwewant.fyi 4 | branch: master # Branch to update (optional; defaults to master) 5 | 6 | # Uncomment below to enable drafts 7 | # publish_mode: editorial_workflow 8 | 9 | media_folder: "static/img" # Media files will be stored in the repo under images/uploads 10 | 11 | collections: 12 | # Wants 13 | - name: "devs" 14 | label: "Dev" 15 | folder: "devs" 16 | create: true 17 | slug: "{{number}}" 18 | fields: 19 | - { label: "Title", name: "title", widget: "string" } 20 | - { label: "Number", name: "number", widget: "number" } 21 | - { label: "Tags", name: "tags", widget: "list" } 22 | - { label: "Description", name: "body", widget: "markdown" } 23 | # Pages 24 | - name: "pages" 25 | label: "Page" 26 | folder: "pages" 27 | slug: "{{slug}}" 28 | fields: 29 | - { label: "Title", name: "title", widget: "string" } 30 | - { label: "Permalink", name: "permalink", widget: "string" } 31 | - { label: "Navigation Title", name: "navtitle", widget: "string" } 32 | - { label: "Tags", name: "tags", widget: "hidden", default: "nav" } 33 | - { label: "Body", name: "body", widget: "markdown" } 34 | -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Netlify CMS 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /admin/preview-templates/index.js: -------------------------------------------------------------------------------- 1 | import Post from "/admin/preview-templates/post.js"; 2 | import Page from "/admin/preview-templates/page.js"; 3 | 4 | // Register the Post component as the preview for entries in the blog collection 5 | CMS.registerPreviewTemplate("blog", Post); 6 | CMS.registerPreviewTemplate("pages", Page); 7 | 8 | CMS.registerPreviewStyle("/_includes/assets/css/inline.css"); 9 | // Register any CSS file on the home page as a preview style 10 | fetch("/") 11 | .then(response => response.text()) 12 | .then(html => { 13 | const f = document.createElement("html"); 14 | f.innerHTML = html; 15 | Array.from(f.getElementsByTagName("link")).forEach(tag => { 16 | if (tag.rel == "stylesheet" && !tag.media) { 17 | CMS.registerPreviewStyle(tag.href); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /admin/preview-templates/page.js: -------------------------------------------------------------------------------- 1 | import htm from "https://unpkg.com/htm?module"; 2 | 3 | const html = htm.bind(h); 4 | 5 | // Preview component for a Page 6 | const Page = createClass({ 7 | render() { 8 | const entry = this.props.entry; 9 | 10 | return html` 11 |
12 |

${entry.getIn(["data", "title"], null)}

13 | 14 | ${this.props.widgetFor("body")} 15 |
16 | `; 17 | } 18 | }); 19 | 20 | export default Page; 21 | -------------------------------------------------------------------------------- /admin/preview-templates/post.js: -------------------------------------------------------------------------------- 1 | import htm from "https://unpkg.com/htm?module"; 2 | import format from "https://unpkg.com/date-fns@2.0.0-alpha.2/esm/format/index.js?module"; 3 | 4 | const html = htm.bind(h); 5 | 6 | // Preview component for a Post 7 | const Post = createClass({ 8 | render() { 9 | const entry = this.props.entry; 10 | 11 | return html` 12 |
13 |
14 |

${entry.getIn(["data", "title"], null)}

15 |

16 | 17 | 25 | ${" by Author"} 26 | 27 |

28 | 29 |

${entry.getIn(["data", "summary"], "")}

30 | 31 | ${this.props.widgetFor("body")} 32 |

33 | ${ 34 | entry.getIn(["data", "tags"], []).map( 35 | tag => 36 | html` 37 | 38 | ` 39 | ) 40 | } 41 |

42 |
43 |
44 | `; 45 | } 46 | }); 47 | 48 | export default Post; 49 | -------------------------------------------------------------------------------- /devs/devs.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/dev.njk" 3 | } -------------------------------------------------------------------------------- /devs/tagged.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: collections 4 | size: 1 5 | alias: tag 6 | filter: 7 | - nav 8 | - all 9 | - wants 10 | - posts 11 | - tags 12 | addAllPagesToCollections: true 13 | layout: layouts/base.njk 14 | renderData: 15 | title: Devs Tagged “{{ tag | unslug | fixNames }}” 16 | permalink: /devs/tagged/{{ tag }}/ 17 | --- 18 | 19 |

Tagged “{{ tag | unslug | fixNames }}”

20 | 21 | {% set devs = collections[ tag ] %} 22 | {% include "components/devs.njk" %} -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The Web We Want", 3 | "short_name": "WWW.fyi", 4 | "description": "If you build websites, you inevitably run into problems. Maybe there’s no way to achieve an aspect of your design using CSS. Or maybe there’s a device feature you really wish you could tap into using JavaScript. Or perhaps the in-browser DevTools don’t give you a key insight you need to do your job. We want to hear about it!", 5 | "background_color": "#cef6da", 6 | "display": "minimal-ui", 7 | "start_url": "/", 8 | "icons": [{ 9 | "src": "/static/img/icon.svg", 10 | "type": "image/svg+xml" 11 | }, 12 | { 13 | "src": "/static/img/android-icon-36x36.png", 14 | "sizes": "36x36", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "/static/img/android-icon-48x48.png", 19 | "sizes": "48x48", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "/static/img/android-icon-72x72.png", 24 | "sizes": "72x72", 25 | "type": "image/png" 26 | }, 27 | { 28 | "src": "/static/img/android-icon-96x96.png", 29 | "sizes": "96x96", 30 | "type": "image/png" 31 | }, 32 | { 33 | "src": "/static/img/android-icon-144x144.png", 34 | "sizes": "144x144", 35 | "type": "image/png" 36 | }, 37 | { 38 | "src": "/static/img/android-icon-192x192.png", 39 | "sizes": "192x192", 40 | "type": "image/png" 41 | } 42 | ], 43 | "shortcuts": [{ 44 | "name": "Home", 45 | "description": "Go to the Homepage", 46 | "url": "/" 47 | }, { 48 | "name": "Share Your Want", 49 | "description": "Whart do you want to see on the web?", 50 | "url": "/#submit" 51 | }, 52 | { 53 | "name": "Wants", 54 | "description": "See a list of all wants", 55 | "url": "/wants/" 56 | }, 57 | { 58 | "name": "Events", 59 | "description": "See what events we’ll be discussing wants at", 60 | "url": "/events/" 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "_site" 3 | command = "eleventy" 4 | 5 | [[headers]] 6 | for = "/*" 7 | [headers.values] 8 | X-Content-Type-Options = "nosniff" 9 | 10 | [[headers]] 11 | for = "/" 12 | [headers.values] 13 | X-Frame-Options = "DENY" 14 | X-XSS-Protection = "1; mode=block" 15 | Cache-Control = "no-cache" 16 | 17 | [[headers]] 18 | for = "/*.html" 19 | [headers.values] 20 | Content-Type = "text/html; charset=utf-8" 21 | X-Frame-Options = "DENY" 22 | X-XSS-Protection = "1; mode=block" 23 | Cache-Control = "no-cache" 24 | 25 | [[headers]] 26 | for = "/site.webmanifest" 27 | [headers.values] 28 | Content-Type = "application/manifest+json; charset=utf-8" 29 | Cache-Control = "no-cache" 30 | 31 | [[headers]] 32 | for = "/*.png" 33 | [headers.values] 34 | Cache-Control = "max-age=31536000, immutable" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devsofcolour", 3 | "title": "Devs of Colour", 4 | "description": "Database of developers of colour of all skill levels, languages, backgrounds.", 5 | "version": "1.0.0", 6 | "scripts": { 7 | "start": "npx eleventy --serve", 8 | "build": "npx eleventy", 9 | "watch": "npx eleventy --watch", 10 | "debug": "DEBUG=* npx eleventy" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/tatianamac/devsofcolour" 15 | }, 16 | "author": { 17 | "name": "Aaron Gustafson", 18 | "email": "aaron@easy-designs.net", 19 | "url": "https://www.aaron-gustafson.com/" 20 | }, 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/tatianamac/devsofcolour/issues" 24 | }, 25 | "homepage": "https://devsofcolour.org", 26 | "devDependencies": { 27 | "@11ty/eleventy": "^0.8.3", 28 | "@11ty/eleventy-plugin-inclusive-language": "^1.0.0", 29 | "dotenv": "^6.2.0", 30 | "lodash": "^4.17.15", 31 | "luxon": "^1.17.1", 32 | "markdown-it": "^9.0.1", 33 | "markdown-it-anchor": "^5.2.4", 34 | "node-fetch": "^2.3.0", 35 | "sanitize-html": "^1.20.0", 36 | "widont": "^0.3.3" 37 | }, 38 | "dependencies": { 39 | "@11ty/eleventy-plugin-syntaxhighlight": "^2.0.3", 40 | "clean-css": "^4.2.1", 41 | "html-minifier": "^4.0.0", 42 | "uglify-es": "^3.3.9" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About us 3 | permalink: /about/ 4 | navtitle: About 5 | tags: 6 | - nav 7 | --- 8 | 9 | Something goes here… 10 | 11 | ## Team 12 | 13 | * [Tatiana Mac](https://twitter.com/TatianaTMac) — Project Founder 14 | * [Aaron Gustafson](https://twitter.com/aarongustafson) 15 | 16 | Interested in getting involved? [Reach out!](/contact) -------------------------------------------------------------------------------- /pages/contact.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/contact.njk 3 | title: Contact Us 4 | description: Have a question? Need help with something? Drop us a line. 5 | date: 2019-01-04T00:00:00.000Z 6 | permalink: /contact/ 7 | --- 8 | 9 | Have a question? Need help with something? Drop us a line below. 10 | 11 | *Please note that all fields are required, but we **will not** retain any of the information you submit beyond the time it takes us to address your message.* -------------------------------------------------------------------------------- /pages/home.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/home.njk 3 | title: Devs of Colour 4 | description: Database of developers of colour of all skill levels, languages, backgrounds. 5 | date: 2019-01-01T00:00:00.000Z 6 | permalink: / 7 | navtitle: Home 8 | tags: 9 | - nav 10 | --- 11 | 12 | Content to come 13 | -------------------------------------------------------------------------------- /pages/offline.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/offline.njk 3 | title: You’re offline 4 | date: 2019-01-04T00:00:00.000Z 5 | permalink: /offline/ 6 | --- 7 | 8 | You appear to be offline. Let us see if we can help. -------------------------------------------------------------------------------- /pages/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/page.njk" 3 | } 4 | -------------------------------------------------------------------------------- /pages/submitted.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/submitted.njk 3 | title: Profile Received! 4 | date: 2019-01-04T00:00:00.000Z 5 | permalink: /submitted/ 6 | --- 7 | 8 | Thanks! -------------------------------------------------------------------------------- /pages/thanks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Message received! 3 | date: 2019-01-04T00:00:00.000Z 4 | permalink: /thanks/ 5 | --- 6 | 7 | We got your message and will be in touch shortly if you’ve requested a response. 8 | -------------------------------------------------------------------------------- /pages/wants.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/devs.njk 3 | title: The Devs of Colour 4 | description: Get to know the developers of colour. 5 | date: 2019-01-02T00:00:00.000Z 6 | navtitle: Devs 7 | tags: 8 | - nav 9 | --- 10 | 11 | Get to know the {{ collections.devs.length }} devs who are a part of our database. Want to join them? [Submit your profile](/submit)! 12 | -------------------------------------------------------------------------------- /static/img/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/android-icon-144x144.png -------------------------------------------------------------------------------- /static/img/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/android-icon-192x192.png -------------------------------------------------------------------------------- /static/img/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/android-icon-36x36.png -------------------------------------------------------------------------------- /static/img/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/android-icon-48x48.png -------------------------------------------------------------------------------- /static/img/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/android-icon-72x72.png -------------------------------------------------------------------------------- /static/img/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/android-icon-96x96.png -------------------------------------------------------------------------------- /static/img/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-114x114.png -------------------------------------------------------------------------------- /static/img/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-120x120.png -------------------------------------------------------------------------------- /static/img/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-144x144.png -------------------------------------------------------------------------------- /static/img/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-152x152.png -------------------------------------------------------------------------------- /static/img/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-180x180.png -------------------------------------------------------------------------------- /static/img/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-57x57.png -------------------------------------------------------------------------------- /static/img/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-60x60.png -------------------------------------------------------------------------------- /static/img/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-72x72.png -------------------------------------------------------------------------------- /static/img/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-76x76.png -------------------------------------------------------------------------------- /static/img/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon-precomposed.png -------------------------------------------------------------------------------- /static/img/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/apple-icon.png -------------------------------------------------------------------------------- /static/img/droid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/favicon-16x16.png -------------------------------------------------------------------------------- /static/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/favicon-32x32.png -------------------------------------------------------------------------------- /static/img/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/favicon-96x96.png -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/flying-car.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/hologram-dash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/hologram-folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/icon.png -------------------------------------------------------------------------------- /static/img/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/img/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/ms-icon-144x144.png -------------------------------------------------------------------------------- /static/img/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/ms-icon-150x150.png -------------------------------------------------------------------------------- /static/img/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/ms-icon-310x310.png -------------------------------------------------------------------------------- /static/img/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/ms-icon-70x70.png -------------------------------------------------------------------------------- /static/img/netlify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /static/img/og-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/og-icon.png -------------------------------------------------------------------------------- /static/img/repeating-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfdefined/devsofcolour/d558c7af6bff65e3b258f5913d585d767a4c5b5f/static/img/repeating-pattern.png -------------------------------------------------------------------------------- /static/js/indieconfig.js: -------------------------------------------------------------------------------- 1 | window.loadIndieConfig = (function () { 2 | 'use strict'; 3 | 4 | // Indie-Config Loading script 5 | // by Pelle Wessman, voxpelli.com 6 | // MIT-licensed 7 | // http://indiewebcamp.com/indie-config 8 | 9 | var config, configFrame, configTimeout, 10 | callbacks = [], 11 | handleConfig, parseConfig; 12 | 13 | // When the configuration has been loaded – deregister all loading mechanics and call all callbacks 14 | handleConfig = function () { 15 | config = config || {}; 16 | 17 | configFrame.parentNode.removeChild(configFrame); 18 | configFrame = undefined; 19 | 20 | window.removeEventListener('message', parseConfig); 21 | 22 | clearTimeout(configTimeout); 23 | 24 | while (callbacks[0]) { 25 | callbacks.shift()(config); 26 | } 27 | }; 28 | 29 | // When we receive a message, check if the source is right and try to parse it 30 | parseConfig = function (message) { 31 | var correctSource = (configFrame && message.source === configFrame.contentWindow); 32 | 33 | if (correctSource && config === undefined) { 34 | try { 35 | config = JSON.parse(message.data); 36 | } catch (ignore) {} 37 | 38 | handleConfig(); 39 | } 40 | }; 41 | 42 | if (!window.navigator.registerProtocolHandler) { 43 | config = {}; 44 | } 45 | 46 | return function (callback) { 47 | // If the config is already loaded, call callback right away 48 | if (config) { 49 | callback(config); 50 | return; 51 | } 52 | 53 | // Otherwise add the callback to the queue 54 | callbacks.push(callback); 55 | 56 | // Are we already trying to load the Indie-Config, then wait 57 | if (configFrame) { 58 | return; 59 | } 60 | 61 | // Create the iframe that will load the Indie-Config 62 | configFrame = document.createElement('iframe'); 63 | configFrame.src = 'web+action:load'; 64 | document.getElementsByTagName('body')[0].appendChild(configFrame); 65 | configFrame.style.display = 'none'; 66 | 67 | // Listen for messages so we will catch the Indie-Config message 68 | window.addEventListener('message', parseConfig); 69 | 70 | // And if no such Indie-Config message has been loaded in a while, abort the loading 71 | configTimeout = setTimeout(handleConfig, 3000); 72 | }; 73 | }()); -------------------------------------------------------------------------------- /static/js/sharing.js: -------------------------------------------------------------------------------- 1 | /* ! Sharing popup */ 2 | (function( window, document ){ 3 | 'use strict'; 4 | 5 | // Filter older browsers 6 | if ( ! ( 'querySelectorAll' in document ) ) 7 | { 8 | return; 9 | } 10 | 11 | // gather the links container 12 | var share_links = document.querySelectorAll('.share, .button--vote'), 13 | watcher_count = share_links.length, 14 | w_threshold = 540, 15 | h_threshold = 340; 16 | 17 | // event handler 18 | function click(e) 19 | { 20 | var target = e.target; 21 | 22 | // target must be an anchor and the inner width threshold must be met 23 | if ( ( target.matches( 'a *' ) || target.matches( '.button' ) ) && 24 | window.innerWidth >= w_threshold && 25 | window.innerHeight >= h_threshold ) 26 | { 27 | // prevent the default link click 28 | e.preventDefault(); 29 | 30 | while ( target.nodeName.toLowerCase() != 'a' ) 31 | { 32 | target = target.parentNode; 33 | } 34 | 35 | // open the link in a popup 36 | window.open( target.href, 'share-this', 'height=300,width=500,status=no,toolbar=no' ); 37 | 38 | // return 39 | return false; 40 | } 41 | } 42 | 43 | // watcher 44 | while ( watcher_count-- ) 45 | { 46 | share_links[watcher_count].addEventListener( 'click', click, false ); 47 | } 48 | 49 | }( this, this.document )); -------------------------------------------------------------------------------- /static/js/webaction.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var loadingClassRegexp = /(^|\s)indieconfig-loading(\s|$)/; 5 | 6 | var doTheAction = function (indieConfig) { 7 | var href, action, anchors; 8 | 9 | // Don't block the tag anymore as the queued action is now handled 10 | this.className = this.className.replace(loadingClassRegexp, ' '); 11 | 12 | // Pick the correct endpoint for the correct action 13 | action = this.getAttribute('do'); 14 | 15 | if ((action === 'reply' || action === 'post') && indieConfig.reply) { 16 | href = indieConfig.reply; 17 | } else if (action === 'like' && indieConfig.like) { 18 | href = indieConfig.like; 19 | } else if (action === 'repost' && indieConfig.repost) { 20 | href = indieConfig.repost; 21 | } else if (action === 'tip' && indieConfig.tip) { 22 | href = indieConfig.tip; 23 | } 24 | 25 | // If no endpoint is found, try the URL of the first a-tag within it 26 | if (!href) { 27 | anchors = this.getElementsByTagName('a'); 28 | if (anchors[0]) { 29 | href = anchors[0].href; 30 | } 31 | } 32 | 33 | // We have found an endpoint! 34 | if (href) { 35 | //Resolve a relative target 36 | var target = document.createElement('a'); 37 | target.href = this.getAttribute('with'); 38 | target = target.href; 39 | 40 | // Insert the target into the endpoint 41 | href = href.replace('{url}', encodeURIComponent(target || window.location.href)); 42 | 43 | // And redirect to it 44 | window.location = href; 45 | } 46 | }; 47 | 48 | // Event handler for a click on an indie-action tag 49 | var handleTheAction = function (e) { 50 | // Prevent the default of eg. any a-tag fallback within the indie-action tag 51 | e.preventDefault(); 52 | 53 | // Make sure this tag hasn't already been queued for the indieconfig-load 54 | if (window.loadIndieConfig && !loadingClassRegexp.test(this.className)) { 55 | this.className += ' indieconfig-loading'; 56 | // Set "doTheAction" to be called when the indie-config has been loaded 57 | window.loadIndieConfig(doTheAction.bind(this)); 58 | } 59 | }; 60 | 61 | // Used to activate all webactions in a context 62 | var activateWebActionElements = function (context) { 63 | var actions = context.querySelectorAll('indie-action'), 64 | i, 65 | length = actions.length; 66 | 67 | for (i = 0; i < length; i++) { 68 | actions[i].addEventListener('click', handleTheAction); 69 | } 70 | }; 71 | 72 | window.activateWebActionElements = activateWebActionElements; 73 | 74 | // Once the page is loaded add click event listeners to all indie-action tags 75 | if (document.body) { 76 | activateWebActionElements(document); 77 | } else { 78 | window.addEventListener('DOMContentLoaded', function () { 79 | activateWebActionElements(document); 80 | }); 81 | } 82 | }()); -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | const version = "v1:", // be sure to update 2 | 3 | // Stuff to load on install 4 | offline_page = "/offline/", 5 | preinstall = [ 6 | // images 7 | "/static/img/favicon-16x16.png", 8 | "/static/img/favicon-32x32.png", 9 | "/static/img/favicon-96x96.png", 10 | // CSS 11 | // JavaScript 12 | // Offline 13 | offline_page 14 | ], 15 | 16 | // caches 17 | sw_caches = { 18 | static: { 19 | name: `${version}static` 20 | }, 21 | pages: { 22 | name: `${version}pages`, 23 | limit: 5 24 | }, 25 | other: { 26 | name: `${version}other`, 27 | limit: 5 28 | } 29 | }, 30 | 31 | // Never cache 32 | ignore = [ 33 | 'thanks', 34 | 'submitted', 35 | 'chrome-extension' 36 | ], 37 | 38 | // How to decide what gets cached and 39 | // what might not be left out 40 | high_priority = [ 41 | /webwewant\.fyi/, 42 | /fonts\.googleapis\.com/ 43 | ], 44 | 45 | fetch_config = { 46 | images: { 47 | mode: 'no-cors' 48 | } 49 | }; 50 | 51 | let slow_connection = false, 52 | save_data; 53 | 54 | if ( 'connection' in navigator ) 55 | { 56 | slow_connection = ( navigator.connection.downlink < .5 ); 57 | save_data = navigator.connection.saveData; 58 | } 59 | self.addEventListener( "activate", event => { 60 | 61 | // console.log('WORKER: activate event in progress.'); 62 | 63 | // clean up stale caches 64 | event.waitUntil( 65 | caches.keys() 66 | .then( keys => { 67 | return Promise.all( 68 | keys 69 | .filter( key => { 70 | return ! key.startsWith( version ); 71 | }) 72 | .map( key => { 73 | return caches.delete( key ); 74 | }) 75 | ); // end promise 76 | }) // end then 77 | ); // end event 78 | 79 | 80 | }); 81 | 82 | addEventListener("message", messageEvent => { 83 | if (messageEvent.data == "clean up") 84 | { 85 | for ( let key in sw_caches ) 86 | { 87 | if ( sw_caches[key].limit != undefined ) 88 | { 89 | trimCache( sw_caches[key].name, sw_caches[key].limit ); 90 | } 91 | } 92 | } 93 | }); 94 | 95 | function trimCache( cache_name, limit ) 96 | { 97 | caches.open( cache_name ) 98 | .then( cache => { 99 | cache.keys() 100 | .then( items => { 101 | if ( items.length > limit ) { 102 | cache.delete( items[0] ) 103 | .then( 104 | trimCache( cache_name, limit) 105 | ); // end delete 106 | } // end if 107 | }); // end keys 108 | }); // end open 109 | } 110 | self.addEventListener( "fetch", event => { 111 | 112 | // console.log( "WORKER: fetch event in progress." ); 113 | 114 | const request = event.request, 115 | url = request.url; 116 | 117 | if ( request.method !== "GET" || shouldBeIgnored( url ) ) 118 | { 119 | // console.log( "ignoring " + url ); 120 | return; 121 | } 122 | 123 | if ( save_data == undefined ) 124 | { 125 | save_data = request.headers.get("save-data"); 126 | } 127 | 128 | // console.log(request.url, request.headers); 129 | 130 | // JSON & such 131 | if ( /\.json$/.test( url ) || 132 | /jsonp\=/.test( url ) ) 133 | { 134 | event.respondWith( 135 | caches.match( request ) 136 | .then( cached_result => { 137 | // cached first 138 | if ( cached_result ) 139 | { 140 | // Update the cache in the background, but only if we’re not trying to save data 141 | if ( ! save_data && ! slow_connection ) 142 | { 143 | event.waitUntil( 144 | refreshCachedCopy( request, sw_caches.other.name ) 145 | ); 146 | } 147 | return cached_result; 148 | } 149 | // fallback to network 150 | return fetch( request ) 151 | .then( response => { 152 | const copy = response.clone(); 153 | event.waitUntil( 154 | saveToCache( sw_caches.other.name, request, copy ) 155 | ); 156 | return response; 157 | }) 158 | // fallback to offline page 159 | .catch( 160 | respondWithServerOffline 161 | ); 162 | }) 163 | ); 164 | } 165 | 166 | // JavaScript 167 | else if ( /\.js$/.test( url ) && isHighPriority( url ) ) 168 | { 169 | event.respondWith( 170 | caches.match( request ) 171 | .then( cached_result => { 172 | // cached first 173 | if ( cached_result ) 174 | { 175 | // Update the cache in the background, but only if we’re not trying to save data 176 | if ( ! save_data && ! slow_connection ) 177 | { 178 | event.waitUntil( 179 | refreshCachedCopy( request, sw_caches.static.name ) 180 | ); 181 | } 182 | return cached_result; 183 | } 184 | // fallback to network 185 | return fetch( request ) 186 | .then( response => { 187 | const copy = response.clone(); 188 | event.waitUntil( 189 | saveToCache( sw_caches.static.name, request, copy ) 190 | ); 191 | return response; 192 | }) 193 | // fallback to offline page 194 | .catch( 195 | respondWithServerOffline 196 | ); 197 | }) 198 | ); 199 | } 200 | 201 | // Wants 202 | else if ( /wants/.test( url ) ) 203 | { 204 | event.respondWith( 205 | fetch( request ) 206 | .then( response => { 207 | const copy = response.clone(); 208 | event.waitUntil( 209 | saveToCache( sw_caches.pages.name, request, copy ) 210 | ); // end waitUntil 211 | return response; 212 | }) 213 | // fallback to offline page 214 | .catch( 215 | caches.match( request ) 216 | .then( cached_result => { 217 | return cached_result; 218 | }) 219 | .catch( 220 | respondWithOfflinePage 221 | ) 222 | ) 223 | ); 224 | } 225 | 226 | // Other HTML 227 | else if ( request.headers.get("Accept").includes("text/html") || 228 | requestIsLikelyForHTML( url ) ) 229 | { 230 | event.respondWith( 231 | // check the cache first 232 | caches.match( request ) 233 | .then( cached_result => { 234 | if ( cached_result ) 235 | { 236 | // Update the cache in the background, but only if we’re not trying to save data 237 | if ( ! save_data && ! slow_connection ) 238 | { 239 | event.waitUntil( 240 | refreshCachedCopy( request, sw_caches.pages.name ) 241 | ); 242 | } 243 | return cached_result; 244 | } 245 | // fallback to network, but cache the result 246 | return fetch( request ) 247 | .then( response => { 248 | const copy = response.clone(); 249 | event.waitUntil( 250 | saveToCache( sw_caches.pages.name, request, copy ) 251 | ); // end waitUntil 252 | return response; 253 | }) 254 | // fallback to offline page 255 | .catch( 256 | respondWithOfflinePage 257 | ); 258 | }) 259 | ); 260 | } 261 | 262 | // images - cache first, then determine if we should request form the network & cache, fallbacks 263 | else if ( request.headers.get("Accept").includes("image") ) 264 | { 265 | event.respondWith( 266 | // check the cache first 267 | caches.match( request ) 268 | .then( cached_result => { 269 | if ( cached_result ) 270 | { 271 | return cached_result; 272 | } 273 | 274 | // high priority imagery 275 | if ( isHighPriority( url ) ) 276 | { 277 | return fetch( request, fetch_config.images ) 278 | .then( response => { 279 | const copy = response.clone(); 280 | event.waitUntil( 281 | saveToCache( sw_caches.static.name, request, copy ) 282 | ); // end waitUntil 283 | return response; 284 | }); 285 | } 286 | // all others 287 | else 288 | { 289 | // console.log('other images', url); 290 | // save data? 291 | if ( save_data || slow_connection ) 292 | { 293 | return new Response( "", { 294 | status: 408, 295 | statusText: "This request was ignored to save data." 296 | }); 297 | } 298 | 299 | // normal operation 300 | else 301 | { 302 | // console.log('fetching'); 303 | return fetch( request, fetch_config.images ) 304 | .then( response => { 305 | const copy = response.clone(); 306 | event.waitUntil( 307 | saveToCache( sw_caches.static.name, request, copy ) 308 | ); 309 | return response; 310 | }); 311 | } 312 | } 313 | }) 314 | ); 315 | } 316 | 317 | // everything else - cache first, then network 318 | else 319 | { 320 | event.respondWith( 321 | // check the cache first 322 | caches.match( request ) 323 | .then( cached_result => { 324 | if ( cached_result ) 325 | { 326 | return cached_result; 327 | } 328 | 329 | // save data? 330 | if ( save_data || slow_connection ) 331 | { 332 | return new Response( "", { 333 | status: 408, 334 | statusText: "This request was ignored to save data." 335 | }); 336 | } 337 | 338 | // normal operation 339 | else 340 | { 341 | return fetch( request ) 342 | .then( response => { 343 | const copy = response.clone(); 344 | if ( isHighPriority( url ) ) 345 | { 346 | event.waitUntil( 347 | saveToCache( sw_caches.static.name, request, copy ) 348 | ); 349 | } 350 | else 351 | { 352 | event.waitUntil( 353 | saveToCache( sw_caches.other.name, request, copy ) 354 | ); 355 | } 356 | return response; 357 | }) 358 | // fallback to offline image 359 | .catch( 360 | respondWithServerOffline 361 | ); 362 | } 363 | }) 364 | ); 365 | } 366 | 367 | }); 368 | 369 | self.addEventListener( "install", function( event ){ 370 | 371 | // console.log( "WORKER: install event in progress." ); 372 | 373 | // immediately take over 374 | self.skipWaiting(); 375 | 376 | event.waitUntil( 377 | caches.open( sw_caches.static.name ) 378 | .then(function( cache ){ 379 | return cache.addAll( preinstall ); 380 | }) 381 | ); 382 | 383 | }); 384 | 385 | function saveToCache( cache_name, request, response ) 386 | { 387 | // console.log( 'saving a copy of', request.url ); 388 | caches.open( cache_name ) 389 | .then( cache => { 390 | return cache.put( request, response ); 391 | }); 392 | } 393 | 394 | function refreshCachedCopy( the_request, cache_name ) 395 | { 396 | fetch( the_request ) 397 | .then( the_response => { 398 | caches.open( cache_name ) 399 | .then( the_cache => { 400 | return the_cache.put( the_request, the_response ); 401 | }); 402 | }); 403 | } 404 | 405 | function shouldBeIgnored( url ) 406 | { 407 | let i = ignore.length; 408 | while( i-- ) 409 | { 410 | if ( url.indexOf( ignore[i] ) > -1 ) 411 | { 412 | // console.log( "found", ignore[i], 'in', url ); 413 | return true; 414 | } 415 | } 416 | return false; 417 | } 418 | 419 | function isHighPriority( url ) 420 | { 421 | let i = high_priority.length; 422 | while ( i-- ) 423 | { 424 | if ( high_priority[i].test( url ) ) 425 | { 426 | return true; 427 | } 428 | } 429 | return false; 430 | } 431 | 432 | function respondWithOfflinePage() 433 | { 434 | return caches.match( offline_page ) 435 | .catch( 436 | respondWithServerOffline 437 | ); 438 | } 439 | 440 | function respondWithServerOffline(){ 441 | return new Response( "", { 442 | status: 408, 443 | statusText: "The server appears to be offline." 444 | }); 445 | } 446 | 447 | function requestIsLikelyForHTML( url ) 448 | { 449 | const final_segment = url.split("/").pop(); 450 | if ( final_segment == "" || 451 | /.+\.html$/.test( final_segment ) || 452 | ! (/\..+$/.test( final_segment ) ) ) 453 | { 454 | return true; 455 | } 456 | return false; 457 | } --------------------------------------------------------------------------------