├── .gitignore ├── .gitmodules ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── build.sh ├── date.js ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package-lock.json ├── package.json ├── src ├── client │ └── apolloClient.js ├── components │ ├── article │ │ ├── headerMeta.js │ │ ├── headerTags.js │ │ ├── postHero.js │ │ ├── postTitleHeader.js │ │ ├── renderAuthorDetails.js │ │ ├── renderComments.js │ │ └── renderMainArticle.js │ ├── authorList.js │ ├── avatar.js │ ├── bio.js │ ├── layout.js │ ├── layout │ │ ├── header.js │ │ ├── headerBar.js │ │ ├── link.js │ │ ├── postRollArticle.js │ │ ├── postRollArticleHero.js │ │ ├── postRollArticleShimmer.js │ │ ├── postRollFooter.js │ │ ├── postRollRenderedArticle.js │ │ ├── sidebarAuthor.js │ │ ├── sidebarRecent.js │ │ └── sidebarTags.js │ ├── layoutAuthor.js │ ├── layoutPost.js │ ├── postBrief.js │ ├── postRollAll.js │ ├── postRollByAuthorId.js │ ├── postRollByTagName.js │ ├── seo.js │ ├── sidebar │ │ ├── renderSidebarAuthorBioParts.js │ │ ├── renderSidebarAuthorDetails.js │ │ ├── renderSidebarTags.js │ │ ├── sidebarAllTags.js │ │ └── sidebarTagDetails.js │ ├── siteFooter.js │ └── tagList.js ├── hooks │ ├── addUtteranceComments.js │ ├── fetchImage.js │ ├── getAuthorDetails.js │ ├── getPostAuthors.js │ ├── getPostTagDetails.js │ ├── getPosts.js │ ├── getPostsByTagName.js │ ├── getSiteLogoSrc.js │ └── static │ │ ├── getAuthorDetailsStatic.js │ │ ├── getFormattedTags.js │ │ ├── getGithubLogoStatic.js │ │ ├── getPostTagDetailsStatic.js │ │ ├── getPostTagsStatic.js │ │ ├── getPostsByAuthorIdStatic.js │ │ ├── getPostsByTagNameStatic.js │ │ ├── getPostsStatic.js │ │ ├── getSiteDetailsStatic.js │ │ └── getSiteLogoSrcStatic.js ├── img │ ├── discord-logo.svg │ ├── facebook-logo.svg │ ├── github-logo-white.svg │ ├── github-logo.svg │ ├── instagram-logo.svg │ ├── linkedin-logo.svg │ ├── logo.svg │ └── twitter-logo.svg ├── pages │ ├── 404.js │ └── index.js ├── scss │ ├── animations.scss │ ├── article.scss │ ├── base.scss │ ├── footer.scss │ ├── index.scss │ ├── layout.scss │ ├── mixins.scss │ ├── postroll.scss │ ├── syntax.scss │ ├── table.scss │ └── vars.scss ├── templates │ ├── author.js │ ├── blog-post.js │ └── tag.js └── utils │ ├── className.js │ ├── customSlug.js │ ├── date.js │ ├── mapPostsToTags.js │ └── mapTagDetailsToPostsTagsMap.js ├── static ├── CNAME ├── favicon.ico └── robots.txt └── uid.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | .idea 72 | 73 | #submodule 74 | content 75 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "content"] 2 | path = content 3 | url = git@github.com:thejsdevsite/jsdev-content.git 4 | branch = main 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.13.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | git: 3 | submodules: false 4 | before_install: 5 | - sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules 6 | - git submodule update --init --recursive 7 | node_js: 8 | - 12.13.0 9 | before_script: 10 | - npm install -g gatsby-cli 11 | deploy: 12 | provider: script 13 | script: . ./build.sh 14 | skip_cleanup: true 15 | keep_history: true 16 | on: 17 | branch: master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2020 Gatsby Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Gatsby 4 | 5 |

6 | 7 | # JS.dev 8 | [![Build Status][build-status]][build-url] 9 | [![Contribute on Github][gh-shield]][thejscgh] 10 | [![Node Version 12.13.0][node-shield]][node12130] 11 | 12 | [JS.dev][jsdev] is an open and transparent JavaScript development community. We welcome and publish contributions from members of the community, for a diverse range of topics covering development with JavaScript. 13 | 14 | Our website is built with [GatsbyJS][gatsbyjs] and hosted on [Github Pages][gh-pages]. If you would like to request a feature, you can do so on our issues page, or submit through a pull request. 15 | 16 | ## 📝 Contributing 17 | 18 | Our blog pages are a combination of markdown files and frontmatter formatting. To submit a new article, [create a pull request][thejscgh] on the content repository, and follow the guide in that project. 19 | 20 | ## 🚀 Building locally 21 | 22 | This project makes use of git submodules: 23 | 24 | * [jsdev-content][thejscgh] for the blog content 25 | 26 | This project makes use of the [nvm][nvm] version manager, with [node version][node12130] `12.13.0`. To setup: 27 | 28 | ```bash 29 | # Make sure the version of node exists first 30 | nvm install 12.13.0 31 | ``` 32 | 33 | On Linux/OSX/WSL: 34 | ```bash 35 | nvm use 36 | ``` 37 | 38 | On Windows (use [Windows nvm][winnvm]): 39 | ```bash 40 | # Windows terminal you have to use specific version 41 | nvm use 12.13.0 42 | ``` 43 | 44 | Run `npm install` to download and install the project dependencies. The easiest way to get GatsbyJS up and running in your local environment is to download the GatsbyJS CLI tools: 45 | 46 | ```bash 47 | npm i -g gatsby gatsby-cli 48 | ``` 49 | 50 | At this point, your `content` directory will be empty. Let's go ahead and set that up: 51 | 52 | ```bash 53 | git submodule update --init --recursive 54 | ``` 55 | 56 | We can now use GatsbyJS CLI to start development in a local environment: 57 | 58 | ```bash 59 | gatsby develop 60 | ``` 61 | 62 | Gives us two local URLs: 63 | 64 | * Localhost development site: [http://localhost:8080](http://localhost:8080) 65 | * GraphQL: [http://localhost:8080/__graphql](http://localhost:8080/__graphql) 66 | 67 | ## 💫 Deploy 68 | 69 | To build for production deployment, run the GatsbyJS `serve` command. Assets such as `sitemap.xml`, `feed.rss` and `robots.txt` will become available, while the GraphQL server will not be started. 70 | 71 | ```bash 72 | npm run clean && npm run build && npm run serve 73 | ``` 74 | 75 | You can view the deployed site locally: 76 | 77 | * [http://localhost:9000/](http://localhost:9000/) 78 | 79 | [node12130]: https://nodejs.org/en/blog/release/v12.13.0/ 80 | [thejscgh]: https://github.com/thejsdevsite/jsdev-content/ 81 | [gh-shield]: https://img.shields.io/badge/style-github-orange?style=flat-square&label=contribute%20on&logo=github 82 | [node-shield]: https://img.shields.io/badge/style-12.13.0-brightgreen?style=flat-square&label=node 83 | [build-status]: https://travis-ci.com/thejsdevsite/jsdev.svg?branch=master 84 | [build-url]: https://travis-ci.com/thejsdevsite/jsdev 85 | [jsdev]: https://thejs.dev 86 | [gatsbyjs]: http://gatsbyjs.com 87 | [gh-pages]: https://pages.github.com/ 88 | [nvm]: https://github.com/nvm-sh/nvm 89 | [winnvm]: https://github.com/coreybutler/nvm-windows -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | setup_git() { 4 | git config --global user.email "$TRAVIS_EMAIL" 5 | git config --global user.name "$TRAVIS_NAME" 6 | } 7 | 8 | do_build() { 9 | npm run updatemodules 10 | npm i 11 | npm run deploy:ci 12 | } 13 | 14 | setup_git 15 | do_build -------------------------------------------------------------------------------- /date.js: -------------------------------------------------------------------------------- 1 | const { exec } = require("child_process"); 2 | 3 | exec(`date -u +"%Y-%m-%dT%H:%M:%S.000Z"`, (error, stdout, stderr) => { 4 | if (error) { 5 | console.log(`error: ${error.message}`); 6 | return; 7 | } 8 | if (stderr) { 9 | console.log(`stderr: ${stderr}`); 10 | return; 11 | } 12 | console.log(`Date: ${stdout}`); 13 | }); -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | //import "typeface-montserrat" 2 | //import "typeface-merriweather" 3 | //import "prismjs/themes/prism.css" 4 | require("./src/scss/index.scss"); 5 | /*import React from 'react'; 6 | import client from './src/client/apolloClient'; 7 | import {ApolloProvider} from '@apollo/client'; 8 | 9 | export const wrapRootElement = ({element}) => ( 10 | {element} 11 | );*/ -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: `JS.dev Community 👨🏼‍💻🚀🎓`, 4 | started: '2020', 5 | author: { 6 | name: `JS.dev`, 7 | summary: `An open and transparent JavaScript development community.`, 8 | }, 9 | description: `An open and transparent JavaScript development community.`, 10 | siteUrl: `https://thejs.dev`, 11 | social: { 12 | twitter: `thejsdevsite`, 13 | github: `thejsdevsite/jsdev`, 14 | facebook: `thejsdevsite`, 15 | instagram: `thejsdevsite`, 16 | linkedIn: `company/thejsdevsite`, 17 | discord: `761215390104944640` 18 | }, 19 | }, 20 | plugins: [ 21 | { 22 | resolve: `gatsby-plugin-google-analytics`, 23 | options: { 24 | trackingId: `UA-41724751-10`, 25 | head: true, 26 | anonymize: true, 27 | }, 28 | }, 29 | { 30 | resolve: `gatsby-source-filesystem`, 31 | options: { 32 | path: `${__dirname}/content/content/blog`, 33 | name: `blog`, 34 | }, 35 | }, 36 | { 37 | resolve: `gatsby-source-filesystem`, 38 | options: { 39 | path: `${__dirname}/content/content/assets`, 40 | name: `assets`, 41 | }, 42 | }, 43 | { 44 | resolve: `gatsby-transformer-remark`, 45 | options: { 46 | plugins: [ 47 | { 48 | resolve:"@weknow/gatsby-remark-codepen", 49 | options: { 50 | theme: "dark", 51 | height: 400 52 | } 53 | }, 54 | { 55 | resolve: `gatsby-remark-images`, 56 | options: { 57 | maxWidth: 590, 58 | }, 59 | }, 60 | { 61 | resolve: `gatsby-remark-responsive-iframe`, 62 | options: { 63 | wrapperStyle: `margin-bottom: 1.0725rem`, 64 | }, 65 | }, 66 | `gatsby-remark-autolink-headers`, 67 | `gatsby-remark-prismjs`, 68 | `gatsby-remark-copy-linked-files`, 69 | `gatsby-remark-smartypants`, 70 | { 71 | resolve: "@weknow/gatsby-remark-twitter", 72 | options: { 73 | align: "center", 74 | hideThread: true 75 | } 76 | }, 77 | { 78 | resolve: `gatsby-remark-social-cards`, 79 | options: { 80 | title: { 81 | title: "title", 82 | font: "DejaVuSansCondensed", 83 | color: "black", 84 | size: 32, 85 | style: "bold" 86 | }, 87 | meta: { 88 | parts: [ 89 | { field: "primaryAuthor" }, 90 | " » ", 91 | { field: "publishedDate", format: "mmmm dS, yyyy" }, 92 | ], 93 | font: "DejaVuSansCondensed", 94 | color: "black", 95 | size: 16, 96 | style: "bold" 97 | }, 98 | background: "#FFF", 99 | xMargin: 12, 100 | yMargin: 24 101 | } 102 | } 103 | ], 104 | }, 105 | }, 106 | `gatsby-transformer-sharp`, 107 | `gatsby-plugin-sharp`, 108 | { 109 | resolve: `gatsby-plugin-feed`, 110 | options: { 111 | query: `{ 112 | site { 113 | siteMetadata { 114 | title 115 | description 116 | siteUrl 117 | site_url: siteUrl 118 | } 119 | } 120 | }`, 121 | feeds: [ 122 | { 123 | serialize: ({ query: { site, allMarkdownRemark } }) => { 124 | return allMarkdownRemark.edges.map(edge => { 125 | return { 126 | ...edge.node.frontmatter, 127 | ...{ 128 | description: edge.node.description, 129 | date: edge.node.frontmatter.updatedDate || edge.node.frontmatter.publishedDate, 130 | url: site.siteMetadata.siteUrl + edge.node.fields.slug, 131 | guid: site.siteMetadata.siteUrl + edge.node.fields.slug, 132 | custom_elements: [{ "content:encoded": edge.node.html}] 133 | } 134 | } 135 | }) 136 | }, 137 | query: `{ 138 | allMarkdownRemark( 139 | sort: { fields: [frontmatter___publishedDate, frontmatter___publishedDate], order: DESC } 140 | filter: {frontmatter: {published: { eq: true }}} 141 | ) { 142 | edges { 143 | node { 144 | excerpt 145 | html 146 | fields { slug } 147 | frontmatter { 148 | title 149 | description 150 | updatedDate 151 | publishedDate 152 | } 153 | } 154 | } 155 | } 156 | }`, 157 | output: "/rss.xml", 158 | title: "JS.dev RSS Feed" 159 | } 160 | ] 161 | } 162 | }, 163 | { 164 | resolve: `gatsby-plugin-manifest`, 165 | options: { 166 | name: `JS.dev`, 167 | short_name: `JS.dev`, 168 | start_url: `/`, 169 | background_color: `#ffffff`, 170 | theme_color: `#663399`, 171 | display: `minimal-ui`, 172 | icon: `${__dirname}/content/content/assets/jsdev-twitter.png` 173 | } 174 | }, 175 | `gatsby-plugin-react-helmet`, 176 | // this (optional) plugin enables Progressive Web App + Offline functionality 177 | // To learn more, visit: https://gatsby.dev/offline 178 | // `gatsby-plugin-offline`, 179 | { 180 | resolve: `gatsby-source-filesystem`, 181 | options: { 182 | path: `${__dirname}/content/data`, 183 | name: `data`, 184 | }, 185 | }, 186 | `gatsby-transformer-yaml`, 187 | { 188 | resolve: 'gatsby-plugin-sass', 189 | options: { 190 | cssLoaderOptions: { 191 | camelCase: false, 192 | } 193 | } 194 | }, 195 | { 196 | resolve: `gatsby-plugin-apollo`, 197 | options: { 198 | uri: '/__graphql' 199 | } 200 | }, 201 | { 202 | resolve: `gatsby-plugin-sitemap`, 203 | options: { 204 | exclude: [`/*/draft-*`] 205 | } 206 | }, 207 | ], 208 | mapping: { 209 | "MarkdownRemark.frontmatter.author": `AuthorYaml`, 210 | "MarkdownRemark.frontmatter.tags": `TagsYaml` 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`) 2 | const { createFilePath } = require(`gatsby-source-filesystem`) 3 | const { getIsoFormatDate } = require("./src/utils/date") 4 | const { generateCustomPostSlug } = require("./src/utils/customSlug") 5 | 6 | exports.createPages = async ({ graphql, actions, reporter }) => { 7 | await createPages(graphql, actions, reporter); 8 | await createTagNodes(graphql, actions, reporter); 9 | await createAuthorNodes(graphql, actions, reporter); 10 | } 11 | 12 | const createPages = async (graphql, actions, reporter) => { 13 | const { createPage } = actions 14 | 15 | // Define a template for blog post 16 | const blogPost = path.resolve(`./src/templates/blog-post.js`) 17 | 18 | // Get all markdown blog posts sorted by date 19 | const result = await graphql( 20 | `{ 21 | allMarkdownRemark( 22 | sort: { fields: [frontmatter___publishedDate, frontmatter___publishedDate, frontmatter___date], order: DESC } 23 | limit: 1000 24 | ) { 25 | nodes { 26 | id 27 | fields { 28 | slug 29 | } 30 | timeToRead 31 | frontmatter { 32 | date(formatString: "DD MMM, YYYY") 33 | publishedDate(formatString: "DD MMM, YYYY") 34 | updatedDate(formatString: "DD MMM, YYYY") 35 | title 36 | authors 37 | posttags 38 | uid 39 | } 40 | } 41 | } 42 | } 43 | `); 44 | 45 | if (result.errors) { 46 | reporter.panicOnBuild(`There was an error loading the blog posts`, result.errors) 47 | return 48 | } 49 | 50 | const posts = result.data.allMarkdownRemark.nodes 51 | if (posts.length > 0) { 52 | posts.forEach((post, index) => { 53 | const previous = index === posts.length - 1 ? null : posts[index + 1] 54 | const next = index === 0 ? null : posts[index - 1] 55 | 56 | createPage({ 57 | path: post.fields.slug, 58 | component: blogPost, 59 | context: { 60 | authors: post.frontmatter.authors, 61 | slug: post.fields.slug, 62 | previous, 63 | next, 64 | }, 65 | }) 66 | }) 67 | } 68 | } 69 | 70 | const createTagNodes = async (graphql, actions, reporter) => { 71 | const { createPage } = actions; 72 | const result = await graphql(` 73 | query GetTagDetails { 74 | allTagsYaml( 75 | sort: {fields: [tag], order: ASC} 76 | ) { 77 | nodes { 78 | id 79 | tag 80 | fields { 81 | slug 82 | } 83 | } 84 | } 85 | }`); 86 | 87 | if (result.errors) { 88 | reporter.panicOnBuild(`There was an error loading the tags for creating page nodes`, result.errors) 89 | return 90 | } 91 | 92 | const tagPageTemplate = path.resolve(`./src/templates/tag.js`) 93 | const tags = result.data.allTagsYaml.nodes; 94 | tags.forEach(tag => { 95 | createPage({ 96 | path: tag.fields.slug, 97 | component: tagPageTemplate, 98 | context: { 99 | slug: tag.fields.slug, 100 | tag: tag.tag 101 | }, 102 | }); 103 | }); 104 | }; 105 | 106 | const createAuthorNodes = async (graphql, actions, reporter) => { 107 | const { createPage } = actions; 108 | const result = await graphql(` 109 | query GetAuthorDetails { 110 | allAuthorYaml( 111 | sort: {fields: [id], order: ASC} 112 | ) { 113 | nodes { 114 | id 115 | fields { 116 | slug 117 | } 118 | } 119 | } 120 | }`); 121 | 122 | if (result.errors) { 123 | reporter.panicOnBuild(`There was an error loading the authors for creating page nodes`, result.errors) 124 | return 125 | } 126 | 127 | const authorPageTempate = path.resolve(`./src/templates/author.js`) 128 | const authors = result.data.allAuthorYaml.nodes 129 | authors.forEach(author => { 130 | createPage({ 131 | path: author.fields.slug, 132 | component: authorPageTempate, 133 | context: { 134 | slug: author.fields.slug, 135 | author: author.id 136 | }, 137 | }); 138 | }); 139 | }; 140 | 141 | exports.onCreateNode = ({ node, actions, getNode }) => { 142 | const { createNodeField } = actions 143 | 144 | if (node.internal.type === `MarkdownRemark`) { 145 | const value = createFilePath({ node, getNode }) 146 | const path = generateCustomPostSlug({ 147 | id: node.frontmatter.uid, 148 | frontmatter: node.frontmatter, 149 | fields: { 150 | slug: value 151 | }, 152 | published: node.frontmatter.published 153 | }); 154 | 155 | createNodeField({ 156 | name: `slug`, 157 | node, 158 | value: path 159 | }) 160 | } 161 | 162 | if (node.internal.type === `AuthorYaml`) { 163 | createNodeField({ 164 | name: `slug`, 165 | node, 166 | value: `/a/${node.id}` 167 | }) 168 | } 169 | 170 | if (node.internal.type === `TagsYaml`) { 171 | createNodeField({ 172 | name: `slug`, 173 | node, 174 | value: `/t/${node.tag}` 175 | }) 176 | } 177 | } 178 | 179 | exports.createSchemaCustomization = ({ actions }) => { 180 | const { createTypes } = actions 181 | 182 | // Explicitly define the siteMetadata {} object 183 | // This way those will always be defined even if removed from gatsby-config.js 184 | 185 | // Also explicitly define the Markdown frontmatter 186 | // This way the "MarkdownRemark" queries will return `null` even when no 187 | // blog posts are stored inside "content/blog" instead of returning an error 188 | createTypes(` 189 | type SiteSiteMetadata { 190 | author: Author 191 | siteUrl: String 192 | social: Social 193 | } 194 | 195 | type Author { 196 | name: String 197 | summary: String 198 | } 199 | 200 | type Social { 201 | twitter: String 202 | facebook: String 203 | github: String 204 | instagram: String 205 | linkedIn: String 206 | discord: String 207 | } 208 | 209 | type MarkdownRemark implements Node { 210 | frontmatter: Frontmatter 211 | fields: Fields 212 | } 213 | 214 | type Frontmatter { 215 | title: String! 216 | description: String! 217 | date: Date! @dateformat 218 | primaryAuthor: String! 219 | publishedDate: Date @dateformat 220 | updatedDate: Date @dateformat 221 | uid: String! 222 | published: Boolean! 223 | authors: [String!] 224 | tags: [String!] 225 | } 226 | 227 | type Fields { 228 | slug: String 229 | } 230 | `) 231 | } 232 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | const React = require("react") 2 | 3 | const HeadComponents = [ 4 | 5 | ]; 6 | 7 | exports.onRenderBody = ({ 8 | setHeadComponents 9 | }, pluginOptions) => { 10 | setHeadComponents(HeadComponents) 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsdev-blog", 3 | "private": true, 4 | "description": "JS.dev Blog", 5 | "version": "1.3.2", 6 | "author": "Justin Mitchell ", 7 | "bugs": { 8 | "url": "https://github.com/thejsdevsite/jsdev/issues" 9 | }, 10 | "dependencies": { 11 | "@apollo/client": "^3.2.0", 12 | "@weknow/gatsby-remark-codepen": "^0.1.0", 13 | "@weknow/gatsby-remark-twitter": "^0.2.3", 14 | "gatsby": "^2.24.61", 15 | "gatsby-image": "^2.4.19", 16 | "gatsby-plugin-apollo": "^3.0.1", 17 | "gatsby-plugin-feed": "^2.5.12", 18 | "gatsby-plugin-google-analytics": "^2.3.14", 19 | "gatsby-plugin-google-tagmanager": "^2.3.12", 20 | "gatsby-plugin-manifest": "^2.4.30", 21 | "gatsby-plugin-offline": "^3.2.28", 22 | "gatsby-plugin-react-helmet": "^3.3.11", 23 | "gatsby-plugin-sass": "^2.3.13", 24 | "gatsby-plugin-sharp": "^2.6.36", 25 | "gatsby-plugin-sitemap": "^2.4.13", 26 | "gatsby-plugin-typography": "^2.5.11", 27 | "gatsby-remark-copy-linked-files": "^2.3.15", 28 | "gatsby-remark-images": "^3.3.30", 29 | "gatsby-remark-prismjs": "^3.5.13", 30 | "gatsby-remark-responsive-iframe": "^2.4.14", 31 | "gatsby-remark-smartypants": "^2.3.11", 32 | "gatsby-remark-social-cards": "^0.4.1", 33 | "gatsby-source-filesystem": "^2.3.30", 34 | "gatsby-transformer-remark": "^2.8.35", 35 | "gatsby-transformer-sharp": "^2.5.15", 36 | "gatsby-transformer-yaml": "^2.4.12", 37 | "node-sass": "^4.14.1", 38 | "prismjs": "^1.21.0", 39 | "react": "^16.12.0", 40 | "react-dom": "^16.12.0", 41 | "react-helmet": "^5.2.1", 42 | "react-typography": "^0.16.19", 43 | "typeface-merriweather": "0.0.72", 44 | "typeface-montserrat": "0.0.75", 45 | "typography": "^0.16.19", 46 | "typography-theme-wordpress-2016": "^0.16.19", 47 | "uniqid": "^5.2.0" 48 | }, 49 | "devDependencies": { 50 | "gatsby-remark-autolink-headers": "^2.3.14", 51 | "gh-pages": "^3.1.0", 52 | "prettier": "2.1.1" 53 | }, 54 | "homepage": "https://github.com/thejsdevsite/jsdev/issues#readme", 55 | "keywords": [ 56 | "gatsby" 57 | ], 58 | "license": "BSD", 59 | "main": "n/a", 60 | "repository": { 61 | "type": "git", 62 | "url": "git+https://github.com/thejsdevsite/jsdev" 63 | }, 64 | "scripts": { 65 | "build": "gatsby build", 66 | "develop": "gatsby develop", 67 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", 68 | "start": "npm run develop", 69 | "serve": "gatsby serve", 70 | "clean": "gatsby clean", 71 | "test": "exit 0", 72 | "deploy:local": "gatsby build && gh-pages -d public master", 73 | "deploy": "gh-pages -d public master -r https://$GH_TOKEN@github.com/$GH_ORG/$GH_REPO.git", 74 | "deploy:ci": "npm run build && npm run deploy", 75 | "preversion": "npm test", 76 | "postversion": "git push && git push --tags", 77 | "preupdatemodules": "git submodule init && git submodule update", 78 | "updatemodules": "git submodule foreach git pull origin main" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/client/apolloClient.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import {ApolloClient, HttpLink, InMemoryCache} from '@apollo/client'; 3 | 4 | const client = new ApolloClient({ 5 | cache: new InMemoryCache(), 6 | link: new HttpLink({ 7 | uri: '/__graphql', 8 | fetch 9 | }) 10 | }); 11 | 12 | export default client; -------------------------------------------------------------------------------- /src/components/article/headerMeta.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "gatsby"; 3 | import Avatar from "../avatar"; 4 | 5 | const HeaderMeta = ({ post, authors }) => { 6 | return ( 7 | <> 8 |
9 | {authors.map(author => ( 10 |
11 | 12 | 13 | {author.name} 14 | 15 |
16 | ))} 17 |
18 |
19 | {post.frontmatter.publishedDate} 20 | { post.frontmatter.updatedDate ? ( 21 | (updated {post.frontmatter.updatedDate}) 22 | ) : null } 23 | 24 | {post.timeToRead} min read 25 |
26 | 27 | ) 28 | } 29 | 30 | export default HeaderMeta; 31 | -------------------------------------------------------------------------------- /src/components/article/headerTags.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const { Link } = require("gatsby") 3 | 4 | const HeaderTags = ({ tags }) => { 5 | return ( 6 |
7 | {tags.map(tag => ( 8 | 9 | #{tag.tag} 10 | 11 | ))} 12 |
13 | ) 14 | } 15 | 16 | export default HeaderTags; -------------------------------------------------------------------------------- /src/components/article/postHero.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PostHero = ({ post }) => { 4 | const { title, hero } = post.frontmatter; 5 | if (!hero) { 6 | return <> 7 | } 8 | 9 | return ( 10 |
11 | {title} 12 |
13 | ) 14 | } 15 | 16 | export default PostHero; -------------------------------------------------------------------------------- /src/components/article/postTitleHeader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useGetAuthorDetailsStatic from "../../hooks/static/getAuthorDetailsStatic"; 3 | import useGetPostTagDetailsStatic from "../../hooks/static/getPostTagDetailsStatic"; 4 | import HeaderTags from "./headerTags"; 5 | import PostHero from "./postHero"; 6 | import HeaderMeta from "./headerMeta" 7 | 8 | const PostTitleHeader = ({ post }) => { 9 | const { title, authors, posttags, published } = post.frontmatter; 10 | const tagsMap = useGetPostTagDetailsStatic(posttags); 11 | const authorsMap = useGetAuthorDetailsStatic(authors); 12 | 13 | const h1Title = !published ? `[Draft] ${title}` : title; 14 | 15 | return ( 16 |
17 | 18 |
19 |

{h1Title}

20 | 21 | 22 |
23 |
24 | ) 25 | } 26 | 27 | export default PostTitleHeader; -------------------------------------------------------------------------------- /src/components/article/renderAuthorDetails.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Avatar from "../avatar"; 3 | import { Link } from "gatsby"; 4 | import githubImage from "../../img/github-logo.svg"; 5 | import twitterImage from "../../img/twitter-logo.svg"; 6 | import facebookImage from "../../img/facebook-logo.svg"; 7 | 8 | export const RenderAuthorDetails = ({ post, authors }) => { 9 | return ( 10 | <> 11 | { authors.map((author, i) => ) } 12 | 13 | ); 14 | }; 15 | 16 | const AuthorsCard = ({ post, author }) => { 17 | const icons = []; 18 | if (author.social) { 19 | const { github, twitter, facebook } = author.social; 20 | if (github) { 21 | icons.push({ 22 | id: "github", 23 | src: githubImage, 24 | href: `https://github.com/${github}`, 25 | title: `Follow ${author.name} on GitHub`, 26 | }); 27 | } 28 | if (facebook) { 29 | icons.push({ 30 | id: "facebook", 31 | src: facebookImage, 32 | href: `https://facebook.com/${facebook}`, 33 | title: `Follow ${author.name} on Facebook`, 34 | }); 35 | } 36 | if (twitter) { 37 | icons.push({ 38 | id: "twitter", 39 | src: twitterImage, 40 | href: `https://twitter.com/${twitter}`, 41 | title: `Follow ${author.name} on Twitter`, 42 | }); 43 | } 44 | } 45 | 46 | return ( 47 |
48 |
49 |
Posted on {post.frontmatter.updatedDate || post.frontmatter.publishedDate }
50 |
51 | 52 | 53 | 54 |
55 |

56 | {author.name} 57 |

58 |
59 | @{author.id} 60 |
61 |
62 | {author.bio} 63 |
64 |
    65 | { icons.map((icon, i) => ( 66 |
  • 67 | 68 | {icon.title} 69 | 70 |
  • 71 | ))} 72 |
73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/article/renderComments.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useAddUtteranceComments from "../../hooks/addUtteranceComments"; 3 | 4 | export const RenderComments = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | }; 13 | 14 | const CommentRenderer = () => { 15 | const commentBox = React.createRef(); 16 | useAddUtteranceComments(commentBox); 17 | return ( 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/article/renderMainArticle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const RenderArticleMain = ({ post }) => { 4 | return ( 5 |
6 |
7 |
8 | ); 9 | }; 10 | 11 | export default RenderArticleMain; 12 | -------------------------------------------------------------------------------- /src/components/authorList.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | 4 | const AuthorList = ({ authorList }) => { 5 | return ( 6 |
11 | { 12 | authorList.map((author, index) => ( 13 | 26 | {author.name} 27 | 28 | )) 29 | } 30 |
31 | ) 32 | } 33 | 34 | export default AuthorList; 35 | -------------------------------------------------------------------------------- /src/components/avatar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Avatar = ({ name, src, className = "" }) => { 4 | const strClass = className ? `jsd-avatar ${className}` : "jsd-avatar"; 5 | if (!src) { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | return ( 13 | 14 | {name} 15 | 16 | ) 17 | } 18 | 19 | export default Avatar; 20 | -------------------------------------------------------------------------------- /src/components/bio.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useStaticQuery, graphql } from "gatsby" 3 | import Image from "gatsby-image" 4 | 5 | const Bio = () => { 6 | const data = useStaticQuery(graphql` 7 | query BioQuery { 8 | avatar: file(absolutePath: { regex: "/profile-pic.jpg/" }) { 9 | childImageSharp { 10 | fixed(width: 50, height: 50) { 11 | ...GatsbyImageSharpFixed 12 | } 13 | } 14 | } 15 | site { 16 | siteMetadata { 17 | author { 18 | name 19 | summary 20 | } 21 | social { 22 | twitter 23 | } 24 | } 25 | } 26 | } 27 | `) 28 | 29 | // Set these values by editing "siteMetadata" in gatsby-config.js 30 | const author = data.site.siteMetadata?.author 31 | const social = data.site.siteMetadata?.social 32 | 33 | const avatar = data?.avatar?.childImageSharp?.fixed 34 | 35 | return ( 36 |
42 | {avatar && ( 43 | {author?.name 56 | )} 57 | {author?.name && ( 58 |

59 | Written by {author.name} {author?.summary || null} 60 | {` `} 61 | 62 | You should follow them on Twitter 63 | 64 |

65 | )} 66 |
67 | ) 68 | } 69 | 70 | export default Bio 71 | -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import HeaderBar from "./layout/headerBar" 3 | import SidebarRecent from "./layout/sidebarRecent" 4 | import SidebarTags from "./layout/sidebarTags" 5 | import SiteFooter from "./siteFooter"; 6 | 7 | const Layout = ({ sidebarTag, children }) => { 8 | 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | 20 |
21 | {children} 22 |
23 | 26 |
27 |
28 |
29 | 30 |
31 | ) 32 | } 33 | 34 | export default Layout 35 | -------------------------------------------------------------------------------- /src/components/layout/header.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | 4 | const Header = ({ location, title }) => { 5 | const rootPath = `${__PATH_PREFIX__}/` 6 | if (location.pathname === rootPath) { 7 | return ( 8 |

{title}

9 | ) 10 | } else { 11 | return ( 12 |

{title}

13 | ) 14 | } 15 | } 16 | 17 | export default Header; -------------------------------------------------------------------------------- /src/components/layout/headerBar.js: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby"; 2 | import React from "react" 3 | import logo from "../../img/logo.svg"; 4 | import { useGetSiteDetailsStatic } from "../../hooks/static/getSiteDetailsStatic"; 5 | import githubWhiteImage from "../../img/github-logo-white.svg"; 6 | 7 | const HeaderBar = () => { 8 | const siteDetails = useGetSiteDetailsStatic(); 9 | return ( 10 |
11 |
12 | 13 | JS.dev 14 | 15 | 16 | Submit an article on GitHub 17 | Submit an article 18 | 19 |
20 |
21 | ) 22 | } 23 | 24 | export default HeaderBar; 25 | -------------------------------------------------------------------------------- /src/components/layout/link.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Link = ({ href, className, id, title, target = "_self" }) => { 4 | const props = { 5 | href, 6 | className, 7 | id, 8 | title, 9 | target, 10 | "aria-label": title || "Link" 11 | }; 12 | 13 | return ( 14 | {children} 15 | ) 16 | } 17 | 18 | export default Link; -------------------------------------------------------------------------------- /src/components/layout/postRollArticle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFetchImageEffect } from "../../hooks/fetchImage"; 3 | import useGetAuthorDetailsStatic from "../../hooks/static/getAuthorDetailsStatic"; 4 | import useGetPostTagDetailsStatic from "../../hooks/static/getPostTagDetailsStatic"; 5 | import PostRollArticleShimmer from "./postRollArticleShimmer"; 6 | import PostRollRenderedArticle from "./postRollRenderedArticle"; 7 | 8 | const PostRollArticle = ({ post }) => { 9 | const authorDetails = useGetAuthorDetailsStatic(post.authors).first(); 10 | const tagDetails = useGetPostTagDetailsStatic(post.tags); 11 | const { loaded: authorImgLoaded, error: authorImgError } = useFetchImageEffect(authorDetails.profilePicture.src); 12 | 13 | if (authorImgError) { 14 | console.error(`Could not load author image (#${authorDetails.id})`); 15 | } 16 | 17 | if ((authorImgLoaded || authorImgError) && authorDetails && tagDetails) { 18 | return 19 | } 20 | 21 | return ; 22 | } 23 | 24 | export default PostRollArticle; 25 | -------------------------------------------------------------------------------- /src/components/layout/postRollArticleHero.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFetchImageEffect } from "../../hooks/fetchImage"; 3 | import useGetAuthorDetailsStatic from "../../hooks/static/getAuthorDetailsStatic"; 4 | import useGetPostTagDetailsStatic from "../../hooks/static/getPostTagDetailsStatic"; 5 | import PostRollArticleShimmer from "./postRollArticleShimmer"; 6 | import PostRollRenderedArticle from "./postRollRenderedArticle"; 7 | 8 | const PostRollArticleHero = ({ post }) => { 9 | const authorDetails = useGetAuthorDetailsStatic(post.authors).first(); 10 | const tagDetails = useGetPostTagDetailsStatic(post.tags); 11 | const { loaded: imgLoaded, error: imgError } = useFetchImageEffect(post.hero); 12 | const { loaded: authorImgLoaded, error: authorImgError } = useFetchImageEffect(authorDetails.profilePicture.src); 13 | 14 | if (authorImgError) { 15 | console.error(`Could not load author image (#${authorDetails.id})`); 16 | } 17 | 18 | if (imgError) { 19 | console.error(`Could not load hero image (#${post.id})`); 20 | } 21 | 22 | if ((imgLoaded || imgError) && (authorImgLoaded || authorImgError) && authorDetails && tagDetails) { 23 | return 24 | } 25 | 26 | return 27 | } 28 | 29 | export default PostRollArticleHero; 30 | -------------------------------------------------------------------------------- /src/components/layout/postRollArticleShimmer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PostRollArticleBasicShimmer = ({ hero = false }) => { 4 | let heroRender = null; 5 | if (hero) { 6 | heroRender = ( 7 |
8 |
9 |
10 | ); 11 | } 12 | 13 | return ( 14 |
15 | { heroRender } 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ) 30 | } 31 | 32 | export default PostRollArticleBasicShimmer; -------------------------------------------------------------------------------- /src/components/layout/postRollFooter.js: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby"; 2 | import React from "react"; 3 | import { className } from "../../utils/className"; 4 | 5 | const PostRollFooter = ({ nextPost, prevPost }) => { 6 | return ( 7 | 33 | ) 34 | } 35 | 36 | export default PostRollFooter; -------------------------------------------------------------------------------- /src/components/layout/postRollRenderedArticle.js: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby"; 2 | import React from "react"; 3 | import { className } from "../../utils/className"; 4 | 5 | const ArticleFeatured = ({ post }) => ( 6 | 7 | 8 | 9 | ) 10 | 11 | const ArticleMeta = ({ post, author }) => ( 12 |
13 |
14 | 15 | {author.name} 16 | 17 |
18 |
19 |

20 | {author.name} 21 |

22 | 23 | 24 | 25 |
26 |
27 | ); 28 | 29 | const ArticleTitle = ({ post }) => ( 30 |

31 | 32 | {post.title} 33 | 34 |

35 | ) 36 | 37 | const ArticleTags = ({ tags }) => ( 38 |
39 | { tags.map(tag => ( 40 | 41 | # 42 | {tag.tag} 43 | 44 | )) } 45 |
46 | ) 47 | 48 | const PostRollRenderedArticle = ({ post, author, tags, hero = false }) => { 49 | return ( 50 |
51 |
52 | {hero ? : null} 53 |
54 |
55 | 56 |
57 |
58 | 59 | 60 |
61 | {post.ttr} min read 62 |
63 |
64 |
65 |
66 |
67 | ) 68 | }; 69 | 70 | export default PostRollRenderedArticle; 71 | -------------------------------------------------------------------------------- /src/components/layout/sidebarAuthor.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useGetAuthorDetailsStatic from "../../hooks/static/getAuthorDetailsStatic"; 3 | import RenderSidebarAuthorDetails from "../sidebar/renderSidebarAuthorDetails"; 4 | 5 | const SidebarAuthor = ({ post }) => { 6 | const authors = useGetAuthorDetailsStatic(post.frontmatter.authors); 7 | const authorList = authors 8 | .all() 9 | .map(node => ) 10 | 11 | return ( 12 | 15 | ) 16 | } 17 | 18 | export default SidebarAuthor; -------------------------------------------------------------------------------- /src/components/layout/sidebarRecent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useGetPostsStatic from "../../hooks/static/getPostsStatic"; 3 | import useGetPostTagDetailsStatic from "../../hooks/static/getPostTagDetailsStatic"; 4 | import { MapPostsToTags } from "../../utils/mapPostsToTags"; 5 | import { MapTagDetailstoPostsTagsMap } from "../../utils/mapTagDetailsToPostsTagsMap"; 6 | import RenderSidebarTags from "../sidebar/renderSidebarTags"; 7 | 8 | const SidebarRecent = () => { 9 | const posts = useGetPostsStatic(25); 10 | const postTagMap = MapPostsToTags(posts); 11 | const tagDetails = useGetPostTagDetailsStatic(postTagMap.keys()); 12 | const tagsPosts = MapTagDetailstoPostsTagsMap(postTagMap, tagDetails); 13 | 14 | const list = tagsPosts 15 | .toArray() 16 | .map(node => ) 17 | 18 | return ( 19 | 22 | ) 23 | } 24 | 25 | export default SidebarRecent; -------------------------------------------------------------------------------- /src/components/layout/sidebarTags.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useGetPostTagDetailsStatic from "../../hooks/static/getPostTagDetailsStatic"; 3 | import useGetFormattedTags from "../../hooks/static/getFormattedTags"; 4 | import SidebarTagDetails from "../sidebar/sidebarTagDetails" 5 | import SidebarAllTags from "../sidebar/sidebarAllTags" 6 | 7 | const SidebarTags = ({ sidebarTag = null }) => { 8 | const tags = useGetFormattedTags(); 9 | let tagDetails = useGetPostTagDetailsStatic(sidebarTag) 10 | tagDetails = tagDetails ? tagDetails.first() : null; 11 | 12 | return ( 13 | 19 | ) 20 | } 21 | 22 | export default SidebarTags; -------------------------------------------------------------------------------- /src/components/layoutAuthor.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import HeaderBar from "./layout/headerBar" 3 | import SidebarAuthor from "./layout/sidebarAuthor" 4 | import SidebarTags from "./layout/sidebarTags" 5 | import SiteFooter from "./siteFooter"; 6 | 7 | const LayoutAuthor = ({ authorId, children }) => { 8 | const post = { 9 | frontmatter: { 10 | authors: [authorId] 11 | } 12 | } 13 | 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 | 25 |
26 | {children} 27 |
28 | 31 |
32 |
33 |
34 | 35 |
36 | ) 37 | } 38 | 39 | export default LayoutAuthor 40 | -------------------------------------------------------------------------------- /src/components/layoutPost.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import HeaderBar from "./layout/headerBar" 3 | import SidebarAuthor from "./layout/sidebarAuthor" 4 | import SiteFooter from "./siteFooter"; 5 | 6 | const LayoutPost = ({ post, children }) => { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 | {children} 16 | 19 |
20 |
21 |
22 | 23 |
24 | ) 25 | } 26 | 27 | export default LayoutPost 28 | -------------------------------------------------------------------------------- /src/components/postBrief.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | import { useGetPostTagsStatic } from "../hooks/static/getPostTagsStatic" 4 | import { useGetPostAuthors } from "../hooks/getPostAuthors"; 5 | import TagList from "../components/tagList"; 6 | import AuthorList from "./authorList"; 7 | 8 | const PostBrief = ({ post, title }) => { 9 | const excerpt = post.frontmatter.description || post.excerpt ? ( 10 |
11 |

17 |

18 | ) : null; 19 | 20 | const tagList = useGetPostTagsStatic(post.frontmatter.posttags); 21 | const authorList = useGetPostAuthors(post.frontmatter.authors); 22 | 23 | return ( 24 |
29 |
30 |

35 | 40 | {title} 41 | 42 |

43 | {post.frontmatter.publishedDate} 44 | 45 | 46 |
47 | {excerpt} 48 |
49 | ) 50 | } 51 | 52 | export default PostBrief; -------------------------------------------------------------------------------- /src/components/postRollAll.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PostRollArticle from "./layout/postRollArticle"; 3 | import PostRollArticleHero from "./layout/postRollArticleHero"; 4 | import useGetPostsStatic from "../hooks/static/getPostsStatic"; 5 | import useGetSiteLogoSrcStatic from "../hooks/static/getSiteLogoSrcStatic"; 6 | 7 | const PostRollAll = ({ offset = 0, limit = 25 }) => { 8 | const posts = useGetPostsStatic(); 9 | const logo = useGetSiteLogoSrcStatic(); 10 | const postList = posts.map((node, index) => { 11 | const { hero, ...data } = node; 12 | data.hero = null; 13 | if (index === 0) { 14 | data.hero = hero ? hero : (logo ? logo : null); 15 | return ; 16 | } 17 | return ; 18 | }) 19 | 20 | return ( 21 |
22 | { postList} 23 |
24 | ) 25 | }; 26 | 27 | export default PostRollAll; -------------------------------------------------------------------------------- /src/components/postRollByAuthorId.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useGetPostsByAuthorIdStatic from "../hooks/static/getPostsByAuthorIdStatic"; 3 | import useGetSiteLogoSrcStatic from "../hooks/static/getSiteLogoSrcStatic"; 4 | import PostRollArticle from "./layout/postRollArticle"; 5 | import PostRollArticleHero from "./layout/postRollArticleHero"; 6 | 7 | const PostRollByAuthorId = ({ authorId }) => { 8 | const posts = useGetPostsByAuthorIdStatic(authorId); 9 | const logo = useGetSiteLogoSrcStatic(); 10 | const postList = posts 11 | .map((node, index) => { 12 | const { hero, ...data } = node; 13 | data.hero = null; 14 | if (index === 0) { 15 | data.hero = hero ? hero : (logo ? logo : null); 16 | return ; 17 | } 18 | return ; 19 | }) 20 | 21 | return ( 22 |
23 | {postList} 24 |
25 | ) 26 | }; 27 | 28 | export default PostRollByAuthorId; -------------------------------------------------------------------------------- /src/components/postRollByTagName.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useGetPostsByTagNameStatic from "../hooks/static/getPostsByTagNameStatic"; 3 | import useGetSiteLogoSrcStatic from "../hooks/static/getSiteLogoSrcStatic"; 4 | import PostRollArticle from "./layout/postRollArticle"; 5 | import PostRollArticleHero from "./layout/postRollArticleHero"; 6 | 7 | const PostRollByTagName = ({ tagName }) => { 8 | const posts = useGetPostsByTagNameStatic(tagName); 9 | const logo = useGetSiteLogoSrcStatic(); 10 | const postList = posts 11 | .map((node, index) => { 12 | const { hero, ...data } = node; 13 | data.hero = null; 14 | if (index === 0) { 15 | data.hero = hero ? hero : (logo ? logo : null); 16 | return ; 17 | } 18 | return ; 19 | }) 20 | 21 | return ( 22 |
23 | {postList} 24 |
25 | ) 26 | }; 27 | 28 | export default PostRollByTagName; -------------------------------------------------------------------------------- /src/components/seo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * Gatsby's useStaticQuery React hook 4 | * 5 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 6 | */ 7 | 8 | import React from "react"; 9 | import PropTypes from "prop-types"; 10 | import { Helmet } from "react-helmet"; 11 | import { graphql, useStaticQuery } from "gatsby"; 12 | import useGetAuthorDetailsStatic from "../hooks/static/getAuthorDetailsStatic"; 13 | import useGetSiteLogoSrcStatic from "../hooks/static/getSiteLogoSrcStatic"; 14 | 15 | const SEO = ({ description, lang, meta, title = undefined, twitterCreator, location, heroImage, twitterDescription, publishedDate = undefined }) => { 16 | const authorDetails = useGetAuthorDetailsStatic(twitterCreator).first(); 17 | const { site } = useStaticQuery( 18 | graphql` 19 | query { 20 | site { 21 | siteMetadata { 22 | title 23 | siteUrl 24 | description 25 | social { 26 | twitter 27 | } 28 | author { 29 | name 30 | } 31 | } 32 | } 33 | } 34 | `, 35 | ); 36 | 37 | const metaDescription = description || site.siteMetadata.description; 38 | const defaultTitle = site.siteMetadata?.title; 39 | const path = (site.siteMetadata.siteUrl + location.pathname).trim("/"); 40 | const metaTitle = title ? `${ title } - ${ defaultTitle }` : defaultTitle; 41 | const logo = useGetSiteLogoSrcStatic(false); 42 | 43 | const metaList = [ 44 | { 45 | name: `description`, 46 | content: metaDescription, 47 | }, 48 | { 49 | property: `og:title`, 50 | content: metaTitle, 51 | }, 52 | { 53 | property: `og:description`, 54 | content: metaDescription, 55 | }, 56 | { 57 | property: `og:type`, 58 | content: `website`, 59 | }, 60 | { 61 | property: `og:image`, 62 | content: `${ site.siteMetadata.siteUrl }${heroImage || logo}`, 63 | }, 64 | { 65 | property: `og:url`, 66 | content: site.siteMetadata.siteUrl + location.pathname, 67 | }, 68 | { 69 | name: `twitter:title`, 70 | content: metaTitle, 71 | }, 72 | { 73 | name: `twitter:description`, 74 | content: twitterDescription || metaDescription, 75 | }, 76 | { 77 | name: `twitter:card`, 78 | content: `summary_large_image`, 79 | }, 80 | { 81 | name: `twitter:site`, 82 | content: `@${ site.siteMetadata.social.twitter }`, 83 | }, 84 | ]; 85 | 86 | if (twitterCreator) { 87 | const twitterCard = path + "/twitter-card.jpg"; 88 | metaList.splice(metaList.findIndex(item => item.property === "og:image"), 1); 89 | metaList.push( 90 | { 91 | name: `twitter:creator`, 92 | content: `@${ authorDetails.social?.twitter }`, 93 | }, 94 | { 95 | name: `author`, 96 | content: authorDetails.name, 97 | }, 98 | { 99 | property: `og:image`, 100 | content: twitterCard 101 | }, 102 | { 103 | name: `twitter:image`, 104 | content: twitterCard, 105 | } 106 | ); 107 | } else { 108 | metaList.push( 109 | { 110 | name: `twitter:creator`, 111 | content: `@${ site.siteMetadata.social.twitter }`, 112 | }, 113 | { 114 | name: `author`, 115 | content: site.siteMetadata.author.name, 116 | }, 117 | ); 118 | } 119 | 120 | if (publishedDate) { 121 | metaList.push({ 122 | name: `article:published_time`, 123 | content: publishedDate 124 | }); 125 | } 126 | 127 | // Assign twitter card 128 | if (!metaList.find(item => item.name === "twitter:image")) { 129 | if (location.pathname.substr(0, 8) === "/author/") { 130 | if (heroImage) { 131 | metaList.push({ 132 | name: `twitter:image`, 133 | content: `${ site.siteMetadata.siteUrl }${ heroImage }`, 134 | }); 135 | } else if (location) { 136 | metaList.push({ 137 | name: `twitter:image`, 138 | content: `${ path }/twitter-card.jpg`, 139 | }); 140 | } 141 | } else if (["/a/", "/t/"].includes(location.pathname.substr(0, 3))) { 142 | metaList.push({ 143 | name: `twitter:image`, 144 | content: `${ site.siteMetadata.siteUrl }${logo}`, 145 | }); 146 | } else if (location.pathname.trim("/") === "tags" || location.pathname.trim("/") === "authors") { 147 | metaList.push({ 148 | name: `twitter:image`, 149 | content: `${ site.siteMetadata.siteUrl }${logo}`, 150 | }); 151 | } else { 152 | metaList.push({ 153 | name: `twitter:image`, 154 | content: `${ site.siteMetadata.siteUrl }${heroImage || logo}`, 155 | }); 156 | } 157 | } 158 | 159 | return ( 160 | 166 | ); 167 | }; 168 | 169 | SEO.defaultProps = { 170 | lang: `en`, 171 | meta: [], 172 | description: ``, 173 | }; 174 | 175 | SEO.propTypes = { 176 | description: PropTypes.string, 177 | lang: PropTypes.string, 178 | meta: PropTypes.arrayOf(PropTypes.object), 179 | title: PropTypes.string, 180 | twitterCreator: PropTypes.string, 181 | }; 182 | 183 | export default SEO; 184 | -------------------------------------------------------------------------------- /src/components/sidebar/renderSidebarAuthorBioParts.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const RenderSidebarAuthorBioParts = ({ author }) => { 4 | const listItems = []; 5 | if (author.employment?.position && author.employment?.company) { 6 | listItems.push({ 7 | id: "work", 8 | title: "Work", 9 | value: ( 10 | <> 11 | {author.employment?.position} 12 | at 13 | { author.employment?.url ? ({author.employment?.company}) : author.employment?.company} 14 | 15 | ) 16 | }) 17 | } 18 | 19 | if (author.location) { 20 | listItems.push({ 21 | id: "location", 22 | title: "Location", 23 | value: author.location 24 | }); 25 | } 26 | 27 | return ( 28 |
29 |
    30 | {listItems.map(item => ( 31 |
  • 32 |
    {item.title}
    33 |
    {item.value}
    34 |
  • 35 | ))} 36 |
37 |
38 | ) 39 | } 40 | 41 | export default RenderSidebarAuthorBioParts; -------------------------------------------------------------------------------- /src/components/sidebar/renderSidebarAuthorDetails.js: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby"; 2 | import React from "react"; 3 | import Avatar from "../avatar"; 4 | import RenderSidebarAuthorBioParts from "./renderSidebarAuthorBioParts"; 5 | 6 | const RenderSidebarAuthorDetails = ({ author }) => { 7 | return ( 8 |
9 |
10 | 11 | 12 | {author.name} 13 | 14 |
15 |
{author.bio}
16 | 17 |
18 | ) 19 | } 20 | 21 | export default RenderSidebarAuthorDetails; 22 | -------------------------------------------------------------------------------- /src/components/sidebar/renderSidebarTags.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "gatsby"; 3 | 4 | const RenderSidebarTags = ({ tagMap }) => { 5 | const { posts, details: tag } = tagMap; 6 | return ( 7 |
8 |
9 |

10 | #{tag.id} 11 |

12 |
13 |
14 | {posts.map(post => { 15 | const ariaTitle = `${post.title} - by ${post.authors.join(" and ")} - published ${post.publishedDate}`; 16 | return {post.title} 17 | })} 18 |
19 |
20 | ) 21 | } 22 | 23 | export default RenderSidebarTags; 24 | -------------------------------------------------------------------------------- /src/components/sidebar/sidebarAllTags.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const { Link } = require("gatsby"); 3 | 4 | const SidebarAllTags = ({ tags }) => { 5 | if (!tags) { 6 | return null; 7 | } 8 | 9 | return ( 10 |
11 |
12 |

Tags

13 |
14 |
15 | 22 |
23 |
24 | ) 25 | } 26 | 27 | export default SidebarAllTags; -------------------------------------------------------------------------------- /src/components/sidebar/sidebarTagDetails.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SidebarTagDetails = ({ tagDetails }) => { 4 | if (!tagDetails) { 5 | return null; 6 | } 7 | 8 | return ( 9 |
10 |
11 |

#{tagDetails.id}

12 |
13 |
14 |

") }} /> 15 |

16 |
17 | ) 18 | } 19 | 20 | export default SidebarTagDetails; -------------------------------------------------------------------------------- /src/components/siteFooter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useGetSiteLogoSrcStatic from "../hooks/static/getSiteLogoSrcStatic"; 3 | import { Link } from "gatsby"; 4 | import { useGetSiteDetailsStatic } from "../hooks/static/getSiteDetailsStatic"; 5 | import githubImage from "../img/github-logo.svg"; 6 | import githubWhiteImage from "../img/github-logo-white.svg"; 7 | import twitterImage from "../img/twitter-logo.svg"; 8 | import facebookImage from "../img/facebook-logo.svg"; 9 | import instagramImage from "../img/instagram-logo.svg"; 10 | import linkedInImage from "../img/linkedin-logo.svg"; 11 | import discordImage from "../img/discord-logo.svg"; 12 | 13 | const SiteFooter = () => { 14 | const siteLogo = useGetSiteLogoSrcStatic(); 15 | const siteDetails = useGetSiteDetailsStatic(); 16 | 17 | const icons = []; 18 | if (siteDetails.social) { 19 | const { github, twitter, facebook, instagram, linkedIn, discord } = siteDetails.social; 20 | if (github) { 21 | icons.push({ 22 | id: "github", 23 | src: githubImage, 24 | href: `https://github.com/${github}`, 25 | title: "Check us out on GitHub", 26 | }); 27 | } 28 | if (twitter) { 29 | icons.push({ 30 | id: "twitter", 31 | src: twitterImage, 32 | href: `https://twitter.com/${twitter}`, 33 | title: "Follow us on Twitter", 34 | }); 35 | } 36 | if (facebook) { 37 | icons.push({ 38 | id: "facebook", 39 | src: facebookImage, 40 | href: `https://facebook.com/${facebook}`, 41 | title: "Follow us on Facebook", 42 | }); 43 | } 44 | if (instagram) { 45 | icons.push({ 46 | id: "instagram", 47 | src: instagramImage, 48 | href: `https://instagram.com/${instagram}`, 49 | title: "Follow us on Instagram", 50 | }); 51 | } 52 | if (linkedIn) { 53 | icons.push({ 54 | id: "linkedIn", 55 | src: linkedInImage, 56 | href: `https://www.linkedin.com/${linkedIn}`, 57 | title: "Follow us on LinkedIn", 58 | }); 59 | } 60 | if (discord) { 61 | icons.push({ 62 | id: "discord", 63 | src: discordImage, 64 | href: `https://www.discord.com/channels/${discord}`, 65 | title: "Join our Discord community", 66 | }); 67 | } 68 | } 69 | 70 | const date = new Date(); 71 | 72 | return ( 73 |
74 |
75 |
76 |
77 |
78 | 79 | {siteDetails.title} 80 | 81 |
82 |

{siteDetails.description}

83 |
    84 | {icons.map(icon => ( 85 |
  • 86 | 87 | {icon.title} 88 | 89 |
  • 90 | ))} 91 |
92 |

JS.dev copyright {siteDetails.started} - {date.getFullYear()}

93 |

Built on GatsbyJS, an open-source static site generator that powers some of the biggest websites.

94 | 100 |
101 | 106 |
107 |
108 |
109 | ); 110 | }; 111 | 112 | export default SiteFooter; 113 | -------------------------------------------------------------------------------- /src/components/tagList.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | 4 | const TagList = ({ tagList }) => { 5 | return ( 6 |
7 | { 8 | tagList.map((tag, index) => ( 9 | 13 | #{tag.tag} 14 | 15 | )) 16 | } 17 |
18 | ) 19 | } 20 | 21 | export default TagList; 22 | -------------------------------------------------------------------------------- /src/hooks/addUtteranceComments.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | const useAddUtteranceComments = (ref) => { 4 | useEffect(() => { 5 | const scriptEl = document.createElement('script') 6 | scriptEl.async = true 7 | scriptEl.src = 'https://utteranc.es/client.js' 8 | scriptEl.setAttribute('repo', 'thejsdevsite/jsdev-discussion') 9 | scriptEl.setAttribute('issue-term', 'title') 10 | scriptEl.setAttribute('id', 'utterances') 11 | scriptEl.setAttribute('theme', 'github-light') 12 | scriptEl.setAttribute('crossorigin', 'anonymous') 13 | scriptEl.setAttribute('label', 'discussion') 14 | if (ref && ref.current) { 15 | ref.current.appendChild(scriptEl) 16 | } else { 17 | console.log(`Error adding utterances comments on: ${ref}`) 18 | } 19 | }, []) 20 | } 21 | 22 | export default useAddUtteranceComments; 23 | -------------------------------------------------------------------------------- /src/hooks/fetchImage.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useFetchImageEffect = (imgSrc) => { 4 | const [ loaded, setLoaded ] = useState(false); 5 | const [ error, setError ] = useState(null); 6 | 7 | useEffect(() => { 8 | (async () => { 9 | if (!imgSrc) { 10 | setError("Cannot fetch image with invalid src"); 11 | setLoaded(false); 12 | return; 13 | } 14 | 15 | if (error) { 16 | setError(null); 17 | } 18 | 19 | try { 20 | const response = await fetch(imgSrc); 21 | if (response.status >= 400) { 22 | throw new Error("Cannot fetch image, non-200 response received"); 23 | } 24 | 25 | setLoaded(true); 26 | } catch (e) { 27 | setError(e.message); 28 | } 29 | })(); 30 | }, [ imgSrc ]); 31 | 32 | return { loaded, error }; 33 | } -------------------------------------------------------------------------------- /src/hooks/getAuthorDetails.js: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client'; 2 | import { useState } from 'react'; 3 | 4 | const query = gql` 5 | query GetAuthorDetails($author: String!) { 6 | allAuthorYaml(filter: {id: {eq: $author}}, limit: 1) { 7 | edges { 8 | node { 9 | id 10 | name 11 | fields { 12 | slug 13 | } 14 | profilePicture { 15 | childImageSharp { 16 | fluid { 17 | src 18 | sizes 19 | srcSet 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | `; 28 | 29 | export const useGetAuthorDetails = (author) => { 30 | const [ hasSet, setHasSet ] = useState(false); 31 | const [ details, setDetails ] = useState({ 32 | profilePicture: { 33 | src: null 34 | } 35 | }); 36 | 37 | const { loading, data, error } = useQuery(query, { 38 | variables: { 39 | author: Array.isArray(author) ? author[0] : "" + author 40 | } 41 | }); 42 | 43 | if (!loading && data && !hasSet) { 44 | const node = data.allAuthorYaml.edges[0].node; 45 | setDetails({ 46 | id: node.id, 47 | name: node.name, 48 | slug: node.fields.slug, 49 | profilePicture: { ...node.profilePicture.childImageSharp.fluid } 50 | }); 51 | setHasSet(true); 52 | } 53 | 54 | return { loading, error, details }; 55 | } 56 | -------------------------------------------------------------------------------- /src/hooks/getPostAuthors.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | export const useGetPostAuthors = (authors) => { 4 | const qlAuthors = useStaticQuery(graphql` 5 | query getAuthorDetails { 6 | allAuthorYaml( 7 | sort: {fields: [id], order: ASC} 8 | ) { 9 | nodes { 10 | id 11 | name 12 | fields { 13 | slug 14 | } 15 | } 16 | } 17 | } 18 | `); 19 | 20 | const predicateAuthors = Array.isArray(authors) ? [...authors] : [authors]; 21 | const { nodes } = qlAuthors.allAuthorYaml; 22 | 23 | return nodes 24 | .filter(node => predicateAuthors.includes(node.id)) 25 | .map(node => ({ 26 | ...node, 27 | slug: node.fields.slug 28 | })); 29 | } -------------------------------------------------------------------------------- /src/hooks/getPostTagDetails.js: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client'; 2 | import { useState } from 'react'; 3 | 4 | const query = gql` 5 | query GetPostTags { 6 | allTagsYaml { 7 | nodes { 8 | id 9 | tag 10 | fields { 11 | slug 12 | } 13 | } 14 | } 15 | } 16 | `; 17 | 18 | export const useGetPostTagDetails = (tags) => { 19 | const [ details, setDetails ] = useState([]); 20 | const predicateTags = Array.isArray(tags) ? [...tags] : [tags]; 21 | const { loading, data, error } = useQuery(query); 22 | 23 | if (data && !error && details.length === 0) { 24 | setDetails(data.allTagsYaml.nodes 25 | .filter(node => predicateTags.includes(node.id)) 26 | .map(node => ({ 27 | id: node.id, 28 | tag: node.tag, 29 | slug: node.fields.slug 30 | })) 31 | ); 32 | } 33 | 34 | return { details, loading, error }; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/hooks/getPosts.js: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client'; 2 | 3 | export const useGetPosts = (offset = 0, limit = 25) => { 4 | const query = gql` 5 | query Posts($offset: Int!, $limit: Int!) { 6 | allMarkdownRemark(filter: {frontmatter: {published: {eq: true}}}, sort: {fields: [frontmatter___date], order: DESC}, limit: $limit, skip: $offset) { 7 | nodes { 8 | id 9 | timeToRead 10 | fields { 11 | slug 12 | } 13 | frontmatter { 14 | date(formatString: "MMMM DD, YYYY") 15 | title 16 | authors 17 | posttags 18 | hero { 19 | childImageSharp { 20 | fluid(jpegQuality: 80, maxWidth: 1280) { 21 | src 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | `; 30 | 31 | return useQuery(query, { 32 | variables: { 33 | offset, 34 | limit 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/hooks/getPostsByTagName.js: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client'; 2 | 3 | const query = gql` 4 | query Posts($tagName: String!, $offset: Int!, $limit: Int!) { 5 | allMarkdownRemark(filter: {frontmatter: {posttags: {in: [$tagName]}, published: {eq: true}}}, sort: {fields: [frontmatter___date], order: DESC}, limit: $limit, skip: $offset) { 6 | nodes { 7 | id 8 | timeToRead 9 | fields { 10 | slug 11 | } 12 | frontmatter { 13 | date(formatString: "MMMM DD, YYYY") 14 | title 15 | authors 16 | posttags 17 | hero { 18 | childImageSharp { 19 | fluid(jpegQuality: 80, maxWidth: 1280) { 20 | src 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | `; 29 | 30 | export const useGetPostsByTagName = (tagName, offset = 0, limit = 25) => { 31 | return useQuery(query, { 32 | variables: { 33 | tagName, 34 | offset, 35 | limit 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/getSiteLogoSrc.js: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client'; 2 | import { useState } from 'react'; 3 | 4 | const query = gql` 5 | query GetSiteLogo { 6 | allFile(filter: {publicURL: {regex: "/.*\/logo\.svg$/"}}) { 7 | nodes { 8 | publicURL 9 | } 10 | } 11 | } 12 | `; 13 | 14 | export const useGetSiteLogoSrc = () => { 15 | const [ src, setSrc ] = useState(null); 16 | const { loading, data, error } = useQuery(query); 17 | 18 | if (data && !src && !loading) { 19 | setSrc(data.allFile.nodes[0].publicURL); 20 | } 21 | 22 | return { loading, error, src }; 23 | } -------------------------------------------------------------------------------- /src/hooks/static/getAuthorDetailsStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | const useGetAuthorDetailsStatic = (authors) => { 4 | const result = useStaticQuery(graphql` 5 | query useGetAuthorDetailsStatic { 6 | allAuthorYaml(sort: {fields: [id], order: ASC}) { 7 | nodes { 8 | id 9 | name 10 | bio 11 | fields { 12 | slug 13 | } 14 | location 15 | employment { 16 | position 17 | company 18 | url 19 | } 20 | social { 21 | twitter 22 | github 23 | facebook 24 | instagram 25 | } 26 | profilePicture { 27 | childImageSharp { 28 | fluid { 29 | src 30 | sizes 31 | srcSet 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | `); 39 | 40 | const predicateAuthors = Array.isArray(authors) ? [...authors] : [authors]; 41 | const { nodes } = result.allAuthorYaml; 42 | 43 | const list = nodes 44 | .filter(node => predicateAuthors.includes(node.id)) 45 | .map(node => ({ 46 | id: node.id, 47 | name: node.name, 48 | slug: node.fields.slug, 49 | profilePicture: { 50 | src: node.profilePicture?.childImageSharp?.fluid?.src 51 | }, 52 | bio: node.bio || "", 53 | location: node.location || "", 54 | employment: { 55 | position: node.employment?.position, 56 | company: node.employment?.company, 57 | url: node.employment?.url 58 | }, 59 | social: { 60 | twitter: node.social?.twitter, 61 | github: node.social?.github, 62 | facebook: node.social?.facebook, 63 | instagram: node.social?.instagram, 64 | } 65 | })); 66 | 67 | return { 68 | first: () => list[0], 69 | last: () => list[list.length - 1], 70 | all: () => list 71 | }; 72 | } 73 | 74 | export default useGetAuthorDetailsStatic; -------------------------------------------------------------------------------- /src/hooks/static/getFormattedTags.js: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from "gatsby" 2 | 3 | const useGetFormattedTags = () => { 4 | const data = useStaticQuery(graphql` 5 | query useGetFormattedTags { 6 | allTagsYaml( 7 | sort: {fields: [tag], order: ASC} 8 | ) { 9 | nodes { 10 | id 11 | tag 12 | fields { 13 | slug 14 | } 15 | } 16 | } 17 | } 18 | `); 19 | 20 | return data.allTagsYaml && 21 | data.allTagsYaml.nodes 22 | .map(node => { 23 | return { 24 | id: node.id, 25 | tag: node.tag, 26 | slug: node.fields.slug 27 | } 28 | }); 29 | } 30 | 31 | export default useGetFormattedTags -------------------------------------------------------------------------------- /src/hooks/static/getGithubLogoStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | const useGetGithubLogoStatic = () => { 4 | const result = useStaticQuery(graphql` 5 | query useGetGithubLogoStatic { 6 | allFile(filter: {publicURL: {regex: "/.*\\/github-logo.png$/"}}) { 7 | nodes { 8 | publicURL 9 | } 10 | } 11 | } 12 | `); 13 | 14 | return (result.allFile.nodes[0].publicURL); 15 | } 16 | 17 | export default useGetGithubLogoStatic; -------------------------------------------------------------------------------- /src/hooks/static/getPostTagDetailsStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | const useGetPostTagDetailsStatic = (tags) => { 4 | const result = useStaticQuery(graphql` 5 | query useGetPostTagDetailsStatic { 6 | allTagsYaml(sort: {fields: [tag], order: ASC}) { 7 | nodes { 8 | id 9 | tag 10 | title 11 | description 12 | foreground 13 | background 14 | fields { 15 | slug 16 | } 17 | } 18 | } 19 | } 20 | `) 21 | 22 | const predicateTags = Array.isArray(tags) ? [...tags] : [tags]; 23 | const list = result.allTagsYaml.nodes 24 | .filter(node => predicateTags.includes(node.id)) 25 | .map(node => ({ 26 | id: node.id, 27 | tag: node.tag, 28 | slug: node.fields.slug, 29 | title: node.title, 30 | description: node.description, 31 | foreground: node.foreground, 32 | background: node.background 33 | })); 34 | 35 | return { 36 | first: () => list[0], 37 | last: () => list[list.length - 1], 38 | all: () => list 39 | } 40 | } 41 | 42 | export default useGetPostTagDetailsStatic -------------------------------------------------------------------------------- /src/hooks/static/getPostTagsStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | export const useGetPostTagsStatic = (tags) => { 4 | const result = useStaticQuery(graphql` 5 | query useGetPostTagsStatic { 6 | allTagsYaml(sort: {fields: [tag], order: ASC}) { 7 | nodes { 8 | id 9 | tag 10 | fields { 11 | slug 12 | } 13 | } 14 | } 15 | } 16 | `) 17 | 18 | const predicateTags = Array.isArray(tags) ? [...tags] : [tags]; 19 | const { nodes } = result.allTagsYaml; 20 | 21 | return nodes 22 | .filter(node => predicateTags.includes(node.tag)) 23 | .map(node => ({ 24 | ...node, 25 | slug: node.fields.slug 26 | })); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/hooks/static/getPostsByAuthorIdStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | const useGetPostsByAuthorIdStatic = (authorId) => { 4 | const result = useStaticQuery(graphql` 5 | query useGetPostsByAuthorIdStatic { 6 | allMarkdownRemark(filter: {frontmatter: {published: {eq: true}}}, sort: {fields: [frontmatter___date], order: DESC}, limit: 1000) { 7 | nodes { 8 | id 9 | timeToRead 10 | fields { 11 | slug 12 | } 13 | frontmatter { 14 | date(formatString: "MMMM DD, YYYY") 15 | title 16 | authors 17 | posttags 18 | hero { 19 | childImageSharp { 20 | fluid(jpegQuality: 80, maxWidth: 1280) { 21 | src 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | `); 30 | 31 | return result.allMarkdownRemark.nodes 32 | .filter(node => node.frontmatter.authors.includes(authorId)) 33 | .map(node => ({ 34 | id: node.id, 35 | ttr: node.timeToRead, 36 | slug: node.fields.slug, 37 | authors: node.frontmatter.authors, 38 | date: node.frontmatter.date, 39 | publishedDate: node.frontmatter.publishedDate, 40 | hero: node.frontmatter.hero ? node.frontmatter.hero.childImageSharp.fluid.src : null, 41 | tags: node.frontmatter.posttags, 42 | title: node.frontmatter.title 43 | })); 44 | } 45 | 46 | export default useGetPostsByAuthorIdStatic; -------------------------------------------------------------------------------- /src/hooks/static/getPostsByTagNameStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | const useGetPostsByTagNameStatic = (tagName) => { 4 | const result = useStaticQuery(graphql` 5 | query useGetPostsByTagNameStatic { 6 | allMarkdownRemark(filter: {frontmatter: {published: {eq: true}}}, sort: {fields: [frontmatter___date], order: DESC}, limit: 1000) { 7 | nodes { 8 | id 9 | timeToRead 10 | fields { 11 | slug 12 | } 13 | frontmatter { 14 | date(formatString: "MMMM DD, YYYY") 15 | publishedDate(formatString: "MMMM DD, YYYY") 16 | title 17 | authors 18 | posttags 19 | hero { 20 | childImageSharp { 21 | fluid(jpegQuality: 80, maxWidth: 1280) { 22 | src 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | `); 31 | 32 | return result.allMarkdownRemark.nodes 33 | .filter(node => node.frontmatter.posttags.includes(tagName)) 34 | .map(node => ({ 35 | id: node.id, 36 | ttr: node.timeToRead, 37 | slug: node.fields.slug, 38 | authors: node.frontmatter.authors, 39 | date: node.frontmatter.date, 40 | publishedDate: node.frontmatter.publishedDate, 41 | hero: node.frontmatter.hero ? node.frontmatter.hero.childImageSharp.fluid.src : null, 42 | tags: node.frontmatter.posttags, 43 | title: node.frontmatter.title 44 | })); 45 | } 46 | 47 | export default useGetPostsByTagNameStatic; -------------------------------------------------------------------------------- /src/hooks/static/getPostsStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | export const useGetPostsStatic = (limit = 0) => { 4 | const result = useStaticQuery(graphql` 5 | query useGetPostsStatic { 6 | allMarkdownRemark(filter: {frontmatter: {published: {eq: true}}}, sort: {fields: [frontmatter___date], order: DESC}, limit: 1000) { 7 | nodes { 8 | id 9 | timeToRead 10 | fields { 11 | slug 12 | } 13 | frontmatter { 14 | date(formatString: "MMMM DD, YYYY") 15 | publishedDate(formatString: "MMMM DD, YYYY") 16 | title 17 | authors 18 | posttags 19 | hero { 20 | childImageSharp { 21 | fluid(jpegQuality: 80, maxWidth: 1280) { 22 | src 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | `); 31 | 32 | const list = result.allMarkdownRemark.nodes 33 | .map(node => ({ 34 | id: node.id, 35 | ttr: node.timeToRead, 36 | slug: node.fields.slug, 37 | authors: node.frontmatter.authors, 38 | date: node.frontmatter.date, 39 | publishedDate: node.frontmatter.publishedDate, 40 | hero: node.frontmatter.hero ? node.frontmatter.hero.childImageSharp.fluid.src : null, 41 | tags: node.frontmatter.posttags, 42 | title: node.frontmatter.title 43 | })); 44 | 45 | if (limit > 0) { 46 | return list.slice(0, limit > list.length ? list.length : limit); 47 | } 48 | 49 | return list; 50 | } 51 | 52 | export default useGetPostsStatic; -------------------------------------------------------------------------------- /src/hooks/static/getSiteDetailsStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby"; 2 | 3 | export const useGetSiteDetailsStatic = () => { 4 | const result = useStaticQuery(graphql` 5 | query useGetSiteDetailsStatic { 6 | site { 7 | siteMetadata { 8 | title 9 | started 10 | description 11 | siteUrl 12 | social { 13 | twitter 14 | github 15 | facebook 16 | linkedIn 17 | instagram 18 | discord 19 | } 20 | } 21 | } 22 | } 23 | `) 24 | 25 | return result.site.siteMetadata; 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/static/getSiteLogoSrcStatic.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from "gatsby" 2 | 3 | const useGetSiteLogoSrcStatic = (svg = true) => { 4 | const result = useStaticQuery(graphql` 5 | query useGetSiteLogoSrcStatic { 6 | allFile(filter: {publicURL: {regex: "/.*\\/logo.(svg|png)$/"}}) { 7 | nodes { 8 | publicURL 9 | } 10 | } 11 | } 12 | `); 13 | 14 | return result.allFile.nodes 15 | .find(node => { 16 | const ext = node.publicURL.substr(node.publicURL.length - 4, 4); 17 | return svg ? ext === ".svg" : ext === ".png"; 18 | }) 19 | .publicURL; 20 | } 21 | 22 | export default useGetSiteLogoSrcStatic; 23 | -------------------------------------------------------------------------------- /src/img/discord-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/facebook-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/github-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/github-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/instagram-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/img/linkedin-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/img/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | import Layout from "../components/layout" 4 | import SEO from "../components/seo" 5 | import { Link } from "gatsby"; 6 | 7 | const NotFoundPage = ({ data, location }) => { 8 | const siteTitle = data.site.siteMetadata?.title || `Title` 9 | 10 | return ( 11 | 12 | 13 |
14 |

This page does not exist

15 |

16 | Return to home page 17 |

18 |
19 |
20 | ) 21 | } 22 | 23 | export default NotFoundPage 24 | 25 | export const pageQuery = graphql` 26 | query { 27 | site { 28 | siteMetadata { 29 | title 30 | } 31 | } 32 | } 33 | ` 34 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | import Layout from "../components/layout" 4 | import SEO from "../components/seo" 5 | import PostRollAll from "../components/postRollAll"; 6 | 7 | const BlogIndex = ({ data, location }) => { 8 | const siteTitle = data.site.siteMetadata?.title || `Title` 9 | 10 | return ( 11 | 12 | 13 |
14 |

Posts

15 |
16 |
17 | 18 |
19 |
20 | ) 21 | } 22 | 23 | export default BlogIndex 24 | 25 | export const pageQuery = graphql` 26 | query { 27 | site { 28 | siteMetadata { 29 | title 30 | } 31 | } 32 | } 33 | ` 34 | -------------------------------------------------------------------------------- /src/scss/animations.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | @import 'mixins'; 3 | 4 | @keyframes placeholderShimmer { 5 | 0% { background-position: -468px 0; } 6 | 100% { background-position: 468px 0; } 7 | } 8 | 9 | @-webkit-keyframes placeholderShimmer { 10 | 0% { background-position: -468px 0; } 11 | 100% { background-position: 468px 0; } 12 | } 13 | 14 | .shimmer { 15 | @include shimmer(); 16 | } -------------------------------------------------------------------------------- /src/scss/article.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | @import "vars"; 3 | @import "mixins"; 4 | 5 | .jsd { 6 | &-card { 7 | &-article { 8 | &-comments { 9 | padding: var(--su-4); 10 | 11 | @media (min-width: $break-tablet) { 12 | padding: var(--su-4) var(--su-8); 13 | } 14 | } 15 | 16 | &-authors { 17 | padding: var(--su-4); 18 | 19 | @media (min-width: $break-tablet) { 20 | padding: var(--su-8) var(--su-12); 21 | } 22 | 23 | > div { 24 | &:first-child { 25 | color: var(--base-a80); 26 | } 27 | } 28 | 29 | &-details { 30 | @include flexbox(); 31 | 32 | ul, li { 33 | padding: 0; 34 | margin: 0; 35 | list-style: none; 36 | } 37 | 38 | ul { 39 | @include flexbox(); 40 | 41 | li { 42 | width: 24px; 43 | height: 24px; 44 | 45 | &.twitter { 46 | height: 23px; 47 | width: 28px; 48 | } 49 | } 50 | 51 | a { 52 | position: relative; 53 | height: 100%; 54 | width: 100%; 55 | display: inline-block; 56 | 57 | img { 58 | @include opacity(0.5); 59 | } 60 | 61 | &:hover { 62 | img { 63 | @include opacity(0.75); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/scss/base.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | @import 'mixins'; 3 | 4 | :root { 5 | --su-0: 0rem; 6 | --su-1: 0.25rem; 7 | --su-2: 0.5rem; 8 | --su-3: 0.75rem; 9 | --su-4: 1rem; 10 | --su-5: 1.25rem; 11 | --su-6: 1.5rem; 12 | --su-7: 1.75rem; 13 | --su-8: 2rem; 14 | --su-9: 2.25rem; 15 | --su-10: 2.5rem; 16 | --su-11: 2.75rem; 17 | --su-12: 3rem; 18 | --su-13: 3.25rem; 19 | --su-14: 3.5rem; 20 | --su-15: 3.75rem; 21 | --su-16: 4rem; 22 | 23 | --mb-0: var(--su-0) !important; 24 | --mb-1: var(--su-1) !important; 25 | --mb-2: var(--su-2) !important; 26 | --mb-3: var(--su-3) !important; 27 | --mb-4: var(--su-4) !important; 28 | --mb-5: var(--su-5) !important; 29 | --mb-6: var(--su-6) !important; 30 | --mb-7: var(--su-7) !important; 31 | --mb-8: var(--su-8) !important; 32 | 33 | --border-radius: 5px; 34 | 35 | --site-width: #{$break-large}; 36 | --header-height: #{$header_height}; 37 | 38 | --ff-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 39 | --ff-monospace: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; 40 | --ff-serif: Palatino, 'Palatino Linotype', 'Palatino LT STD', 'Book Antiqua', Georgia, serif; 41 | --ff-console: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 42 | 43 | --fs-xl: #{$fnt-sz-xl}; 44 | --fs-l: #{$fnt-sz-l}; 45 | --fs-m: #{$fnt-sz-m}; 46 | --fs-base: #{$fnt-sz-base}; 47 | --fs-s: #{$fnt-sz-s}; 48 | --fs-xs: #{$fnt-sz-xs}; 49 | --fs-xxs: #{$fnt-sz-xxs}; 50 | --fw-light: #{$fnt_wt_light}; 51 | --fw-normal: #{$fnt_wt_normal}; 52 | --fw-med: #{$fnt_wt_med}; 53 | --fw-heavy: #{$fnt_wt_bold}; 54 | --fw-bold: #{$fnt_wt_bolder}; 55 | --fs-2xl: #{$fnt-sz-2xl}; 56 | --fs-3xl: #{$fnt-sz-3xl}; 57 | --fs-4xl: var(--su-10); 58 | --fs-5xl: var(--su-10); 59 | 60 | --lh-tight: 1.25; 61 | --lh-tighter: 1.125; 62 | 63 | // Colours 64 | --body-bg: #{$colour_body_grey}; 65 | --body-colour: #{$colour_dark_navy_blue}; 66 | --link-colour: #{$link_colour}; 67 | --link-bg-hover: #{$link_colour_bg}; 68 | --link-colour-hover: #{$link_colour_hover}; 69 | --tag-colour: #{$colour_card_tertiary}; 70 | --tag-colour-hover: #{$colour_tag_hover}; 71 | --card-bg: #{$colour_white}; 72 | --card-colour: #{$colour_card}; 73 | --card-colour-secondary: #{$colour_card_secondary}; 74 | --card-colour-tertiary: #{$colour_card_tertiary}; 75 | --card-secondary-border: rgba(8, 9, 10, 0.05); 76 | --card-secondary-colour: #{$colour_dark_grey_link}; 77 | --card-secondary-bg: #{$colour_beige}; 78 | --card-secondary-hover-bg: #{$colour_yellow}; 79 | --card-secondary-hover-colour: #{$colour_dark_navy_blue}; 80 | 81 | --base: #08090a; 82 | --base-a90: rgba(8, 9, 10, 0.9); 83 | --base-a80: rgba(8, 9, 10, 0.8); 84 | --base-a70: rgba(8, 9, 10, 0.7); 85 | --base-a60: rgba(8, 9, 10, 0.6); 86 | --base-a50: rgba(8, 9, 10, 0.5); 87 | --base-a40: rgba(8, 9, 10, 0.4); 88 | --base-a30: rgba(8, 9, 10, 0.3); 89 | --base-a20: rgba(8, 9, 10, 0.2); 90 | --base-a10: rgba(8, 9, 10, 0.1); 91 | --base-a5: rgba(8, 9, 10, 0.05); 92 | --base-100: var(--base); 93 | --base-90: #202428; 94 | --base-80: #363d44; 95 | --base-70: #4d5760; 96 | --base-60: #64707d; 97 | --base-50: #7d8a97; 98 | --base-40: #99a3ad; 99 | --base-30: #b5bdc4; 100 | --base-20: #d2d6db; 101 | --base-10: #eef0f1; 102 | --base-0: #f9fafa; 103 | --base-inverted: #fff; 104 | 105 | --accent-brand: #{$colour_yellow}; 106 | 107 | --title-font-size: var(--su-6); 108 | 109 | @media (max-width: $break-large) { 110 | --site-width: 100% 111 | } 112 | } 113 | 114 | * { 115 | &::before, &::after { 116 | @include prefix(box-sizing, inherit, browsers); 117 | } 118 | } 119 | 120 | .fs-xl { font-size: var(--fs-xl); } 121 | .fs-l { font-size: var(--fs-l); } 122 | .fs-m { font-size: var(--fs-m); } 123 | .fs-s { font-size: var(--fs-s); } 124 | .fs-xs { font-size: var(--fs-xs); } 125 | .fs-xxs { font-size: var(--fs-xxs); } 126 | .fs-2xl { font-size: var(--fs-2xl); } 127 | .fs-3xl { font-size: var(--fs-3xl); } 128 | .fs-4xl { font-size: var(--fs-4xl); } 129 | .fs-5xl { font-size: var(--fs-5xl); } 130 | .fs-base { font-size: var(--fs-base); } 131 | 132 | .s\:fs-5xl { 133 | @media (min-width: $break-tablet) { 134 | font-size: var(--fs-5xl) !important; 135 | } 136 | } 137 | .s\:fs-4xl { 138 | @media (min-width: $break-tablet) { 139 | font-size: var(--fs-4xl) !important; 140 | } 141 | } 142 | .s\:fs-3xl { 143 | @media (min-width: $break-tablet) { 144 | font-size: var(--fs-3xl) !important; 145 | } 146 | } 147 | .s\:fs-2xl { 148 | @media (min-width: $break-tablet) { 149 | font-size: var(--fs-2xl) !important; 150 | } 151 | } 152 | .s\:fs-xl { 153 | @media (min-width: $break-tablet) { 154 | font-size: var(--fs-xl) !important; 155 | } 156 | } 157 | .s\:fs-l { 158 | @media (min-width: $break-tablet) { 159 | font-size: var(--fs-l) !important; 160 | } 161 | } 162 | .s\:fs-m { 163 | @media (min-width: $break-tablet) { 164 | font-size: var(--fs-m) !important; 165 | } 166 | } 167 | .s\:fs-s { 168 | @media (min-width: $break-tablet) { 169 | font-size: var(--fs-s) !important; 170 | } 171 | } 172 | .s\:fs-xs { 173 | @media (min-width: $break-tablet) { 174 | font-size: var(--fs-xs) !important; 175 | } 176 | } 177 | 178 | .m\:fs-5xl { 179 | @media (min-width: $break-medium) { 180 | font-size: var(--fs-5xl) !important; 181 | } 182 | } 183 | .m\:fs-4xl { 184 | @media (min-width: $break-medium) { 185 | font-size: var(--fs-4xl) !important; 186 | } 187 | } 188 | .m\:fs-3xl { 189 | @media (min-width: $break-medium) { 190 | font-size: var(--fs-3xl) !important; 191 | } 192 | } 193 | .m\:fs-2xl { 194 | @media (min-width: $break-medium) { 195 | font-size: var(--fs-2xl) !important; 196 | } 197 | } 198 | .m\:fs-xl { 199 | @media (min-width: $break-medium) { 200 | font-size: var(--fs-xl) !important; 201 | } 202 | } 203 | .m\:fs-l { 204 | @media (min-width: $break-medium) { 205 | font-size: var(--fs-l) !important; 206 | } 207 | } 208 | .m\:fs-m { 209 | @media (min-width: $break-medium) { 210 | font-size: var(--fs-m) !important; 211 | } 212 | } 213 | .m\:fs-s { 214 | @media (min-width: $break-medium) { 215 | font-size: var(--fs-s) !important; 216 | } 217 | } 218 | .m\:fs-xs { 219 | @media (min-width: $break-medium) { 220 | font-size: var(--fs-xs) !important; 221 | } 222 | } 223 | 224 | .l\:fs-5xl { 225 | @media (min-width: $break-medium) { 226 | font-size: var(--fs-5xl) !important; 227 | } 228 | } 229 | .l\:fs-4xl { 230 | @media (min-width: $break-medium) { 231 | font-size: var(--fs-4xl) !important; 232 | } 233 | } 234 | .l\:fs-3xl { 235 | @media (min-width: $break-medium) { 236 | font-size: var(--fs-3xl) !important; 237 | } 238 | } 239 | .l\:fs-2xl { 240 | @media (min-width: $break-medium) { 241 | font-size: var(--fs-2xl) !important; 242 | } 243 | } 244 | .l\:fs-l { 245 | @media (min-width: $break-medium) { 246 | font-size: var(--fs-l) !important; 247 | } 248 | } 249 | .l\:fs-m { 250 | @media (min-width: $break-medium) { 251 | font-size: var(--fs-m) !important; 252 | } 253 | } 254 | .l\:fs-s { 255 | @media (min-width: $break-medium) { 256 | font-size: var(--fs-s) !important; 257 | } 258 | } 259 | .l\:fs-xs { 260 | @media (min-width: $break-medium) { 261 | font-size: var(--fs-xs) !important; 262 | } 263 | } 264 | .l\:fs-base { 265 | @media (min-width: $break-medium) { 266 | font-size: var(--fs-base) !important; 267 | } 268 | } 269 | 270 | .fw-light { font-weight: var(--fw-light); } 271 | .fw-normal { font-weight: var(--fw-normal); } 272 | .fw-med { font-weight: var(--fw-med); } 273 | .fw-heavy { font-weight: var(--fw-heavy); } 274 | .fw-bold { font-weight: var(--fw-bold); } 275 | .s\:fw-heavy { 276 | @media (min-width: $break-tablet) { 277 | font-weight: var(--fw-heavy) !important; 278 | } 279 | } 280 | 281 | .ff-sans-serif { font-family: var(--ff-sans-serif); } 282 | .ff-monospace { font-family: var(--ff-monospace); } 283 | .ff-serif { font-family: var(--ff-serif); } 284 | 285 | .lh-tight { line-height: var(--lh-tight) !important; } 286 | .lh-tighter { line-height: var(--lh-tighter) !important; } 287 | 288 | .ls-n1 { letter-spacing: -1px !important ;} 289 | .ls-n2 { letter-spacing: -2px !important ;} 290 | .ls-n3 { letter-spacing: -3px !important ;} 291 | .ls-n4 { letter-spacing: -4px !important ;} 292 | 293 | .l\:ls-n1 { 294 | @media (min-width: $break-medium) { 295 | letter-spacing: -1px !important ; 296 | } 297 | } 298 | .l\:ls-n2 { 299 | @media (min-width: $break-medium) { 300 | letter-spacing: -2px !important ; 301 | } 302 | } 303 | .l\:ls-n3 { 304 | @media (min-width: $break-medium) { 305 | letter-spacing: -3px !important ; 306 | } 307 | } 308 | .l\:ls-n4 { 309 | @media (min-width: $break-medium) { 310 | letter-spacing: -4px !important ; 311 | } 312 | } 313 | 314 | .s\:ls-n1 { 315 | @media (min-width: $break-tablet) { 316 | letter-spacing: -1px !important ; 317 | } 318 | } 319 | .s\:ls-n2 { 320 | @media (min-width: $break-tablet) { 321 | letter-spacing: -2px !important ; 322 | } 323 | } 324 | .s\:ls-n3 { 325 | @media (min-width: $break-tablet) { 326 | letter-spacing: -3px !important ; 327 | } 328 | } 329 | .s\:ls-n4 { 330 | @media (min-width: $break-tablet) { 331 | letter-spacing: -4px !important ; 332 | } 333 | } 334 | 335 | .left { 336 | text-align: left; 337 | } 338 | 339 | .right { 340 | text-align: right; 341 | } 342 | 343 | .center { 344 | text-align: center; 345 | margin-left: auto; 346 | margin-right: auto; 347 | } 348 | 349 | .justify { 350 | text-align: justify; 351 | } 352 | 353 | .hidden { 354 | display: none !important; 355 | } 356 | 357 | .hidden-sm { 358 | display: none; 359 | } 360 | 361 | .w-0 { width: 0 !important; } 362 | .w-25 { width: 25% !important; } 363 | .w-50 { width: 50% !important; } 364 | .w-75 { width: 75% !important; } 365 | .w-100 { width: 100% !important; } 366 | .h-0 { height: 0 !important; } 367 | 368 | .m-0 { margin: var(--su-0) !important; } 369 | .m-1 { margin: var(--su-1) !important; } 370 | .m-2 { margin: var(--su-2) !important; } 371 | .m-3 { margin: var(--su-3) !important; } 372 | .m-4 { margin: var(--su-4) !important; } 373 | .m-5 { margin: var(--su-5) !important; } 374 | .m-6 { margin: var(--su-6) !important; } 375 | .m-7 { margin: var(--su-7) !important; } 376 | .m-8 { margin: var(--su-8) !important; } 377 | 378 | .mb-0 { margin-bottom: var(--su-0) !important; } 379 | .mb-1 { margin-bottom: var(--su-1) !important; } 380 | .mb-2 { margin-bottom: var(--su-2) !important; } 381 | .mb-3 { margin-bottom: var(--su-3) !important; } 382 | .mb-4 { margin-bottom: var(--su-4) !important; } 383 | .mb-5 { margin-bottom: var(--su-5) !important; } 384 | .mb-6 { margin-bottom: var(--su-6) !important; } 385 | .mb-7 { margin-bottom: var(--su-7) !important; } 386 | .mb-8 { margin-bottom: var(--su-8) !important; } 387 | 388 | .s\:mb-0 { 389 | @media (min-width: $break-tablet) { 390 | margin-bottom: var(--su-0) !important; 391 | } 392 | } 393 | 394 | .mt-0 { margin-top: var(--su-0) !important; } 395 | .mt-1 { margin-top: var(--su-1) !important; } 396 | .mt-2 { margin-top: var(--su-2) !important; } 397 | .mt-3 { margin-top: var(--su-3) !important; } 398 | .mt-4 { margin-top: var(--su-4) !important; } 399 | .mt-5 { margin-top: var(--su-5) !important; } 400 | .mt-6 { margin-top: var(--su-6) !important; } 401 | .mt-7 { margin-top: var(--su-7) !important; } 402 | .mt-8 { margin-top: var(--su-8) !important; } 403 | .-mt-0 { margin-top: calc(var(--su-0) * -1) !important; } 404 | .-mt-1 { margin-top: calc(var(--su-1) * -1) !important; } 405 | .-mt-2 { margin-top: calc(var(--su-2) * -1) !important; } 406 | .-mt-3 { margin-top: calc(var(--su-3) * -1) !important; } 407 | .-mt-4 { margin-top: calc(var(--su-4) * -1) !important; } 408 | .-mt-5 { margin-top: calc(var(--su-5) * -1) !important; } 409 | .-mt-6 { margin-top: calc(var(--su-6) * -1) !important; } 410 | .-mt-7 { margin-top: calc(var(--su-7) * -1) !important; } 411 | .-mt-8 { margin-top: calc(var(--su-8) * -1) !important; } 412 | 413 | .mb-0 { margin-right: var(--su-0) !important; } 414 | .mr-1 { margin-right: var(--su-1) !important; } 415 | .mr-2 { margin-right: var(--su-2) !important; } 416 | .mr-3 { margin-right: var(--su-3) !important; } 417 | .mr-4 { margin-right: var(--su-4) !important; } 418 | .mr-5 { margin-right: var(--su-5) !important; } 419 | .mr-6 { margin-right: var(--su-6) !important; } 420 | .mr-7 { margin-right: var(--su-7) !important; } 421 | .mr-8 { margin-right: var(--su-8) !important; } 422 | 423 | .ml-0 { margin-left: var(--su-0) !important; } 424 | .ml-1 { margin-left: var(--su-1) !important; } 425 | .ml-2 { margin-left: var(--su-2) !important; } 426 | .ml-3 { margin-left: var(--su-3) !important; } 427 | .ml-4 { margin-left: var(--su-4) !important; } 428 | .ml-5 { margin-left: var(--su-5) !important; } 429 | .ml-6 { margin-left: var(--su-6) !important; } 430 | .ml-7 { margin-left: var(--su-7) !important; } 431 | .ml-8 { margin-left: var(--su-8) !important; } 432 | 433 | .p-0 { padding: var(--su-0) !important; } 434 | .p-1 { padding: var(--su-1) !important; } 435 | .p-2 { padding: var(--su-2) !important; } 436 | .p-3 { padding: var(--su-3) !important; } 437 | .p-4 { padding: var(--su-4) !important; } 438 | .p-5 { padding: var(--su-5) !important; } 439 | .p-6 { padding: var(--su-6) !important; } 440 | .p-7 { padding: var(--su-7) !important; } 441 | .p-8 { padding: var(--su-8) !important; } 442 | 443 | .pt-0 { padding-top: var(--su-0) !important; } 444 | .pt-1 { padding-top: var(--su-1) !important; } 445 | .pt-2 { padding-top: var(--su-2) !important; } 446 | .pt-3 { padding-top: var(--su-3) !important; } 447 | .pt-4 { padding-top: var(--su-4) !important; } 448 | .pt-5 { padding-top: var(--su-5) !important; } 449 | .pt-6 { padding-top: var(--su-6) !important; } 450 | .pt-7 { padding-top: var(--su-7) !important; } 451 | .pt-8 { padding-top: var(--su-8) !important; } 452 | 453 | .py-0 { padding-top: var(--su-0) !important; padding-bottom: var(--su-0) !important; } 454 | .py-1 { padding-top: var(--su-1) !important; padding-bottom: var(--su-1) !important; } 455 | .py-2 { padding-top: var(--su-2) !important; padding-bottom: var(--su-2) !important; } 456 | .py-3 { padding-top: var(--su-3) !important; padding-bottom: var(--su-3) !important; } 457 | .py-4 { padding-top: var(--su-4) !important; padding-bottom: var(--su-4) !important; } 458 | .py-5 { padding-top: var(--su-5) !important; padding-bottom: var(--su-5) !important; } 459 | .py-6 { padding-top: var(--su-6) !important; padding-bottom: var(--su-6) !important; } 460 | .py-7 { padding-top: var(--su-7) !important; padding-bottom: var(--su-7) !important; } 461 | .py-8 { padding-top: var(--su-8) !important; padding-bottom: var(--su-8) !important; } 462 | 463 | .radius-full { 464 | @include borderRadius(9999px, true); 465 | } 466 | 467 | .cursor-pointer { 468 | cursor: pointer; 469 | } 470 | 471 | .grid { 472 | display: grid !important; 473 | } 474 | 475 | .l\:grid { 476 | @media (min-width: $break-medium) { 477 | display: grid !important; 478 | } 479 | } 480 | 481 | .gap-0 { grid-gap: var(--su-0) !important; } 482 | .gap-1 { grid-gap: var(--su-1) !important; } 483 | .gap-2 { grid-gap: var(--su-2) !important; } 484 | .gap-3 { grid-gap: var(--su-3) !important; } 485 | .gap-4 { grid-gap: var(--su-4) !important; } 486 | .gap-5 { grid-gap: var(--su-5) !important; } 487 | .gap-6 { grid-gap: var(--su-6) !important; } 488 | .gap-7 { grid-gap: var(--su-7) !important; } 489 | .gap-8 { grid-gap: var(--su-8) !important; } 490 | 491 | .flex { 492 | display: flex !important; 493 | } 494 | .shrink-0 { 495 | flex-shrink: 0 !important; 496 | } 497 | 498 | .opacity-50 { 499 | @include opacity(0.5); 500 | } 501 | 502 | body { 503 | @include flexCol(); 504 | min-height: calc(100vh - var(--header-height)); 505 | scroll-behavior: smooth; 506 | text-rendering: optimizeSpeed; 507 | line-height: 1.5; 508 | overflow-y: scroll; 509 | padding-top: var(--header-height); 510 | background: var(--body-bg); 511 | color: var(--body-colour); 512 | font-family: $def_font; 513 | font-size: $def_font_size; 514 | font-weight: $def_font_weight; 515 | margin: var(--su-0); 516 | } 517 | 518 | figcaption,figure, div, main, header, footer, section { 519 | display: block; 520 | } 521 | 522 | h1, h2, h3, h4, h5 { 523 | font-family: $def_font; 524 | font-weight: 500; 525 | 526 | & > strong { 527 | font-weight: 600; 528 | } 529 | 530 | &.text-center { 531 | & > strong { 532 | &:after { 533 | margin: 10px auto 0 auto; 534 | } 535 | } 536 | } 537 | 538 | a { 539 | &.anchor { 540 | display: none; 541 | @media (min-width: $break-small) { 542 | display: block; 543 | } 544 | } 545 | } 546 | } 547 | 548 | h1 { 549 | font-size: 2.5rem; 550 | } 551 | 552 | h2 { 553 | font-size: 2.35rem; 554 | } 555 | 556 | h3 { 557 | font-size: 2.15rem 558 | } 559 | 560 | h4 { 561 | font-size: 1.95rem; 562 | } 563 | 564 | h5 { 565 | font-size: 1.75rem; 566 | } 567 | 568 | button, html [type="button"], [type="reset"], [type="submit"] { 569 | @include prefixOnly(appearance, button, $browsers); 570 | } 571 | 572 | .hide { 573 | display: none !important; 574 | } 575 | 576 | .show { 577 | display: block !important; 578 | } 579 | 580 | .show-large { 581 | @media screen and (max-width: $break-large) { 582 | display: $disp_block_imp; 583 | } 584 | } 585 | 586 | .show-small { 587 | @media screen and (max-width: $break-small) { 588 | display: $disp_block_imp; 589 | } 590 | } 591 | 592 | .show-tablet { 593 | @media screen and (max-width: $break-tablet) { 594 | display: $disp_block_imp; 595 | } 596 | } 597 | 598 | .show-mobile { 599 | @media screen and (max-width: $break-mobile) { 600 | display: $disp_block_imp; 601 | } 602 | } 603 | 604 | .hide-large { 605 | @media screen and (max-width: $break-large) { 606 | display: $disp_none_imp; 607 | } 608 | } 609 | 610 | .hide-small { 611 | @media screen and (max-width: $break-small) { 612 | display: $disp_none_imp; 613 | } 614 | } 615 | 616 | .hide-mobile { 617 | @media screen and (max-width: $break-mobile) { 618 | display: $disp_none_imp; 619 | } 620 | } 621 | 622 | .sticky { 623 | @include fixedTop(0, 0, 0); 624 | } 625 | 626 | img { 627 | max-width: 100%; 628 | height: auto; 629 | border-style: none; 630 | } 631 | 632 | .text-center { 633 | text-align: center; 634 | } 635 | 636 | .inner { 637 | max-width: 100%; 638 | } 639 | 640 | .float-right { 641 | float: right; 642 | } 643 | 644 | a { 645 | text-decoration: none; 646 | 647 | &.github { 648 | @include flexRow(); 649 | @include borderRadius(var(--border-radius)); 650 | font-size: var(--fs-xs); 651 | background: #24292E; 652 | color: #fff; 653 | font-family: var(--ff-sans-serif); 654 | line-height: 1; 655 | font-weight: normal; 656 | text-decoration: none; 657 | align-items: center; 658 | height: 36px; 659 | padding: 2px var(--su-3); 660 | 661 | img { 662 | max-width: 24px; 663 | max-height: 24px; 664 | } 665 | 666 | &:hover { 667 | @include opacity(0.75); 668 | } 669 | } 670 | } 671 | 672 | blockquote { 673 | padding: 0 var(--su-2) 0 var(--su-6); 674 | margin: 0 0 var(--su-6); 675 | border-left: 4px solid var(--accent-brand); 676 | color: var(--base-70); 677 | font-family: var(--ff-monospace); 678 | font-size: var(--fs-base); 679 | } 680 | -------------------------------------------------------------------------------- /src/scss/footer.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | @import 'base'; 3 | @import 'mixins'; 4 | 5 | :root { 6 | --site-footer-height: 100px; 7 | --site-footer-bg: rgba(210,214,219,0.26) 8 | } 9 | 10 | .jsd { 11 | &-site { 12 | &-footer { 13 | min-height: var(--site-footer-height); 14 | background: var(--site-footer-bg); 15 | margin-top: var(--su-6); 16 | border-top: 1px solid var(--base-a10); 17 | padding: var(--su-6) var(--su-1); 18 | 19 | @media (min-width: $break-small) { 20 | padding: var(--su-8); 21 | } 22 | 23 | &-container { 24 | &-inner { 25 | @include flexCol(); 26 | margin: auto; 27 | align-items: flex-start; 28 | position: relative; 29 | padding-right: var(--layout-padding); 30 | padding-left: var(--layout-padding); 31 | max-width: var(--site-width); 32 | justify-content: space-between; 33 | 34 | @media (min-width: $break-small) { 35 | @include flexRow(); 36 | } 37 | } 38 | 39 | 40 | position: relative; 41 | @include flexCol(); 42 | justify-content: space-between; 43 | padding: 0 var(--su-2); 44 | 45 | @media (min-width: $break-small) { 46 | @include flexRow(); 47 | justify-content: space-between; 48 | padding: 0 var(--su-4); 49 | } 50 | } 51 | 52 | &-info { 53 | --link-colour: #{$link_colour_hover}; 54 | --link-colour-hover: #{$colour_dark_navy_blue}; 55 | 56 | @include grid(var(--su-4)); 57 | width: 100%; 58 | align-self: flex-start; 59 | font-size: var(--fs-xs); 60 | order: 1; 61 | padding: var(--su-1) var(--su-1) var(--su-1) var(--su-4); 62 | 63 | @media (min-width: $break-small) { 64 | width: 25%; 65 | padding: 0; 66 | margin-right: var(--su-8); 67 | order: initial; 68 | } 69 | 70 | div:not(:first-child) { 71 | @include flexbox(1 auto); 72 | } 73 | 74 | > div:first-child { 75 | a { 76 | @include flexbox(); 77 | } 78 | } 79 | 80 | img { 81 | max-height: 40px; 82 | } 83 | 84 | p { 85 | margin: 0; 86 | } 87 | 88 | ul,li { 89 | margin: 0; 90 | padding: 0; 91 | list-style: none; 92 | } 93 | 94 | ul { 95 | @include flexRow(); 96 | li { 97 | @include flexbox(); 98 | margin-right: var(--su-3); 99 | a { 100 | @include flexbox(); 101 | } 102 | 103 | img { 104 | @include opacity(0.6); 105 | height: 24px; 106 | width: 24px; 107 | } 108 | } 109 | } 110 | } 111 | 112 | &-links { 113 | @include flexauto(1 auto); 114 | @include grid(var(--su-4), repeat(2, 1fr)); 115 | 116 | @media (min-width: $break-tablet) { 117 | grid-template-columns: repeat(3, 1fr); 118 | } 119 | 120 | @media (min-width: $break-medium) { 121 | grid-template-columns: repeat(3, 200px); 122 | } 123 | 124 | a { 125 | @include flexbox(1 auto); 126 | @include borderRadius(var(--border-radius)); 127 | font-size: var(--fs-s); 128 | align-items: center; 129 | height: 36px; 130 | padding-left: var(--su-4); 131 | &:hover { 132 | background: var(--base-a5); 133 | color: #{$colour_dark_navy_blue}; 134 | } 135 | } 136 | } 137 | 138 | &-right { 139 | display: none; 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | @import 'mixins'; 3 | @import 'base'; 4 | @import 'layout'; 5 | @import 'syntax'; 6 | @import 'postroll'; 7 | @import 'animations'; 8 | @import 'footer'; 9 | @import 'article'; 10 | @import 'table'; 11 | 12 | .tag-list { 13 | margin: 0 auto; 14 | width: 100%; 15 | margin-bottom: 1rem; 16 | 17 | a { 18 | font-size: 0.8rem; 19 | border-radius: 5; 20 | padding: 0.25rem; 21 | display: inline-block; 22 | color: #64707d; 23 | text-decoration: none; 24 | box-shadow: none; 25 | 26 | .prefix { 27 | @include opacity(0.4); 28 | font-style: italic; 29 | padding-right: 1px; 30 | } 31 | } 32 | } 33 | 34 | .top-bar { 35 | position: fixed; 36 | top: 0; 37 | right: 0; 38 | left: 0; 39 | z-index: $header_zindex; 40 | height: var(--header-height); 41 | background: $header_bgcolour; 42 | @include boxShadow(0, 1, 1, 0,rgba(0, 0, 0, 0.1)); 43 | 44 | .top-bar-container { 45 | @include flexRow(); 46 | margin: auto; 47 | align-items: center; 48 | position: relative; 49 | padding-right: var(--layout-padding); 50 | padding-left: var(--layout-padding); 51 | max-width: var(--site-width); 52 | height: var(--header-height); 53 | justify-content: space-between; 54 | 55 | a { 56 | @include flexbox(); 57 | margin: 0; 58 | text-decoration: none; 59 | padding: 0 7px 0 9px; 60 | 61 | img { 62 | max-height: $header_logo_max_height; 63 | max-width: $header_logo_max_width; 64 | margin: 0; 65 | height: $header_logo_max_height; 66 | } 67 | 68 | &.github { 69 | img { 70 | max-height: 24px; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/scss/layout.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | @import 'mixins'; 3 | 4 | :root { 5 | --layout-sidebar-left-width: 240px; 6 | --layout-content-width: 2fr; 7 | --layout-sidebar-right-width: 1fr; 8 | --layout-drawer-width: 300px; 9 | --layout: 100%; 10 | --layout-gap: var(--su-2); 11 | --layout-padding: 0; 12 | --content-font-family: var(--ff-sans-serif); 13 | 14 | @media (min-width: $break-small) { 15 | --layout-sidebar-left-width: 2fr; 16 | --layout-padding: var(--su-2); 17 | --layout: var(--layout-content-width); 18 | } 19 | 20 | --article-rythm: var(--su-2); 21 | --article-padding-x: var(--su-3); 22 | --article-padding-y: var(--su-3); 23 | --article-font-size: var(--fs-s); 24 | 25 | @media (min-width: $break-tablet) { 26 | --article-padding-x: var(--su-5); 27 | --article-padding-y: var(--su-5); 28 | } 29 | 30 | @media (min-width: $break-small) { 31 | --article-rythm: var(--su-4); 32 | --article-padding-x: var(--su-8); 33 | --article-padding-y: var(--su-7); 34 | } 35 | } 36 | 37 | [class^='jsd'], 38 | [class^='jsd']::before, 39 | [class^='jsd']::after, 40 | [class^='jsd'] *, 41 | [class^='jsd'] *::before, 42 | [class^='jsd'] *::after { 43 | @include borderBox(); 44 | } 45 | 46 | .universal-page-content-wrapper { 47 | flex: 1 auto; 48 | visibility: visible; 49 | font-size: 18px; 50 | } 51 | 52 | .site-container { 53 | @include flexCol(); 54 | 55 | .page-content { 56 | @include grid(var(--layout-gap), var(--layout)); 57 | padding: var(--layout-padding); 58 | } 59 | } 60 | 61 | .jsd { 62 | &-layout { 63 | @include grid(var(--layout-gap), var(--layout)); 64 | font-size: var(--fs-base); 65 | max-width: var(--site-width); 66 | margin: 0 auto; 67 | padding: var(--layout-padding); 68 | 69 | &-2-cols { 70 | --layout-sidebar-right-display: none; 71 | @media (min-width: $break-small) { 72 | --layout: var(--layout) 0; 73 | } 74 | 75 | @media (min-width: $break-medium) { 76 | --layout-sidebar-right-display: block; 77 | --layout-sidebar-right-width: 1fr; 78 | --layout-content-width: 2.5fr; 79 | --layout: var(--layout-content-width) var(--layout-sidebar-right-width); 80 | } 81 | } 82 | 83 | &-3-cols { 84 | @media (min-width: $break-small) { 85 | --layout: var(--layout-sidebar-left-width) var(--layout-content-width); 86 | --layout-sidebar-left-row-end: span 2; 87 | --layout-sidebar-left-width: 2fr; 88 | --layout-sidebar-right-width: 5fr; 89 | --layout-content-width: 5fr; 90 | } 91 | 92 | @media (min-width: $break-medium) { 93 | --layout-sidebar-left-width: 240px; 94 | --layout-sidebar-right-width: 1fr; 95 | --layout-sidebar-left-row-end: initial; 96 | --layout-content-width: 2fr; 97 | --layout: var(--layout-sidebar-left-width) var(--layout-content-width) var(--layout-sidebar-right-width); 98 | } 99 | 100 | &-drop { 101 | &-right { 102 | &-left { 103 | --layout-sidebar-left-display: none; 104 | --layout-sidebar-right-display: none; 105 | --layout-sidebar-left-width: var(--layout-drawer-width); 106 | --layout-sidebar-right-width: var(--layout-drawer-width); 107 | 108 | @media (min-width: $break-small) { 109 | --layout-sidebar-left-width: 2fr; 110 | --layout-sidebar-left-row-end: initial; 111 | --layout-sidebar-left-display: block; 112 | } 113 | 114 | @media (min-width: $break-medium) { 115 | --layout-sidebar-right-display: block; 116 | --layout-sidebar-right-width: 1fr; 117 | --layout-sidebar-left-width: 240px; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | .jsd { 127 | &-layout { 128 | &-sidebar { 129 | &-left { 130 | display: var(--layout-sidebar-left-display); 131 | grid-row-end: var(--layout-sidebar-left-row-end); 132 | width: var(--layout-sidebar-left-width); 133 | } 134 | 135 | &-right { 136 | display: var(--layout-sidebar-right-display); 137 | grid-row-end: var(--layout-sidebar-right-row-end); 138 | width: var(--layout-sidebar-right-width); 139 | } 140 | } 141 | } 142 | } 143 | 144 | .jsd { 145 | &-header { 146 | &-widget { 147 | .jsd-sidebar-widget { 148 | padding: 0; 149 | } 150 | 151 | .hidden { 152 | display: grid !important; 153 | } 154 | 155 | .jsd-card { 156 | margin-bottom: var(--su-6); 157 | &.branded { 158 | --fs-m: var(--fs-s); 159 | --fs-base: #{$fnt-sz-xs}; 160 | --fs-xs: #{$fnt-sz-xxs}; 161 | --base-50: var(--base-70); 162 | --base-60: var(--base-70); 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | .jsd-sidebar-widget { 170 | position: relative; 171 | display: block; 172 | padding: 0 16px 16px 16px; 173 | margin: 8px 0 16px 0; 174 | 175 | header { 176 | position: relative; 177 | font-weight: var(--fw-heavy); 178 | padding: 8px 0; 179 | font-family: var(--ff-monospace); 180 | color: var(--card-colour); 181 | margin: var(--m-0); 182 | 183 | h2, h3 { 184 | margin: 0 0 var(--su-2) 0; 185 | } 186 | } 187 | 188 | .widget-body { 189 | overflow-wrap: break-word; 190 | 191 | p { 192 | margin: var(--su-1) 0 0 0; 193 | } 194 | } 195 | 196 | &:after { 197 | content: ''; 198 | position: absolute; 199 | bottom: 0; 200 | left: 0; 201 | right: 0; 202 | width: 100%; 203 | height: 1px; 204 | background: var(--card-colour-tertiary); 205 | @include opacity(0.25); 206 | } 207 | 208 | &:last-child{ 209 | &:after { 210 | background: transparent; 211 | } 212 | } 213 | 214 | &:first-child { 215 | header { 216 | padding-top: 0; 217 | } 218 | } 219 | } 220 | 221 | .sidebar-tags-browser { 222 | .sidebar-nav-element { 223 | display: block; 224 | position: relative; 225 | } 226 | } 227 | 228 | .jsd-link { 229 | color: var(--link-colour); 230 | cursor: pointer; 231 | 232 | &:hover { 233 | color: var(--link-colour-hover); 234 | } 235 | } 236 | 237 | .jsd-link-block { 238 | @include flexbox(); 239 | @include borderRadius(var(--border-radius)); 240 | 241 | @media (min-width: 640px) { 242 | padding: var(--su-2); 243 | } 244 | 245 | &:hover { 246 | &:not(.jsd-link-current) { 247 | background: #FDE000; 248 | color: var(--link-colour); 249 | } 250 | } 251 | } 252 | 253 | .jsd-card { 254 | @include borderRadius(var(--border-radius)); 255 | @include boxShadow(0, 0, 0, 1, var(--card-border)); 256 | background: var(--card-bg); 257 | color: var(--card-colour); 258 | } 259 | 260 | .jsd-card-header { 261 | @include flexbox(); 262 | justify-content: space-between; 263 | align-items: center; 264 | padding: var(--su-3) var(--su-4); 265 | border-bottom: 1px solid var(--body-bg); 266 | } 267 | 268 | .jsd-card-secondary { 269 | @include boxShadow(0, 0, 0, 1, var(--card-secondary-border)); 270 | background: var(--card-secondary-bg); 271 | color: var(--card-secondary-color); 272 | 273 | &.branded { 274 | border-top: var(--su-7) solid var(--accent-brand); 275 | } 276 | } 277 | 278 | .jsd-card-header-headline { 279 | font-size: var(--fs-s); 280 | font-family: var(--ff-monospace); 281 | font-weight: var(--fw-bold); 282 | color: var(--base-100); 283 | margin: 0; 284 | 285 | a { 286 | color: inherit; 287 | 288 | &:hover { 289 | color: var(--link-colour-hover); 290 | } 291 | } 292 | } 293 | 294 | .jsd-link-contentful { 295 | display: block; 296 | padding: var(--su-4); 297 | border-bottom: 1px solid var(--body-bg); 298 | font-size: var(--fs-xs); 299 | 300 | &:hover { 301 | font-weight: var(--fw-med); 302 | color: var(--card-secondary-hover-colour); 303 | background: var(--card-secondary-hover-bg); 304 | } 305 | } 306 | 307 | .jsd-article-wrapper { 308 | min-width: 0; 309 | } 310 | 311 | .jsd-article { 312 | header { 313 | border-bottom: 0 none; 314 | 315 | h1 { 316 | margin: 0; 317 | } 318 | } 319 | 320 | --article-padding-x: var(--su-3); 321 | --article-padding-y: var(--su-3); 322 | 323 | @media (min-width: $break-small) { 324 | --article-padding-x: var(--su-12); 325 | --article-padding-y: var(--su-8); 326 | } 327 | } 328 | 329 | .jsd-article-header { 330 | overflow-wrap: anywhere; 331 | word-break: break-word; 332 | } 333 | 334 | .jsd-article-header-meta { 335 | padding: var(--article-padding-y) var(--article-padding-x) 0 var(--article-padding-x); 336 | } 337 | 338 | .jsd-spec-tags { 339 | a { 340 | text-decoration: none; 341 | 342 | &:hover { 343 | text-decoration: none; 344 | } 345 | } 346 | } 347 | 348 | .jsd-tag { 349 | @include borderRadius(var(--border-radius)); 350 | font-size: var(--fs-xs); 351 | color: var(--tag-color); 352 | line-height: 1.3; 353 | padding: var(--su-1) var(--su-2); 354 | display: inline-block; 355 | 356 | @media (min-width: $break-tablet) { 357 | font-size: var(--fs-s); 358 | } 359 | } 360 | 361 | .jsd-article-subheader { 362 | @include flexbox(); 363 | font-size: var(--fs-base); 364 | color: var(--base-60); 365 | flex-wrap: wrap; 366 | align-items: center; 367 | 368 | .jsd-avatar { 369 | width: var(--su-7); 370 | height: var(--su-7); 371 | } 372 | } 373 | 374 | .jsd-article-main { 375 | padding: var(--article-padding-y) var(--article-padding-x); 376 | gap: var(--su-7); 377 | } 378 | 379 | .jsd-article-body { 380 | overflow-wrap: break-word; 381 | } 382 | 383 | .text-styles { 384 | --text-style-fs: var(--su-4); 385 | font-size: var(--text-style-fs); 386 | font-family: var(--content-font-family); 387 | 388 | @media (min-width: $break-small) { 389 | --text-style-fs: var(--fs-s); 390 | } 391 | 392 | h1 { 393 | font-size: var(--su-6); 394 | font-weight: var(--fw-med); 395 | @media (min-width: $break-small) { 396 | font-size: var(--su-8); 397 | font-weight: var(--fw-med); 398 | } 399 | } 400 | 401 | h2 { 402 | font-size: var(--su-6); 403 | font-weight: var(--fw-bold); 404 | @media (min-width: $break-small) { 405 | font-size: var(--su-7); 406 | font-weight: var(--fw-med); 407 | } 408 | } 409 | 410 | h3 { 411 | font-size: var(--su-5); 412 | font-weight: var(--fw-med); 413 | @media (min-width: $break-small) { 414 | font-size: var(--su-7); 415 | } 416 | } 417 | 418 | h4 { 419 | font-size: var(--su-5); 420 | font-weight: var(--fw-med); 421 | @media (min-width: $break-small) { 422 | font-size: var(--su-6); 423 | font-weight: var(--fw-med); 424 | } 425 | } 426 | 427 | h5 { 428 | font-size: var(--su-4); 429 | font-weight: var(--fw-bold); 430 | @media (min-width: $break-small) { 431 | font-size: var(--su-5); 432 | font-weight: var(--fw-bold); 433 | } 434 | } 435 | 436 | h6 { 437 | font-size: var(--su-4); 438 | font-weight: var(--fw-bold); 439 | @media (min-width: $break-small) { 440 | font-size: var(--su-5); 441 | font-weight: var(--fw-med); 442 | } 443 | } 444 | 445 | a { 446 | color: var(--link-colour-hover); 447 | cursor: pointer; 448 | 449 | &:hover { 450 | color: var(--link-colour); 451 | text-decoration: underline; 452 | } 453 | } 454 | 455 | strong { 456 | font-weight: var(--fw-heavy); 457 | } 458 | } 459 | 460 | .jsd-author-bio { 461 | font-size: var(--fs-base); 462 | color: var(--base-70); 463 | } 464 | 465 | .jsd-author-details { 466 | ul { 467 | list-style: none; 468 | margin: 0; 469 | padding: 0; 470 | 471 | li { 472 | margin-bottom: var(--su-4); 473 | } 474 | 475 | .key { 476 | text-transform: uppercase; 477 | color: var(--base-50); 478 | font-size: var(--fs-xs); 479 | font-weight: var(--fw-bold); 480 | } 481 | 482 | .value { 483 | font-size: var(--fs-base); 484 | color: var(--base-60); 485 | 486 | span { 487 | margin: 0 var(--su-2); 488 | } 489 | } 490 | } 491 | } 492 | 493 | .jsd { 494 | &-article { 495 | &-cover { 496 | position: relative; 497 | width: 100%; 498 | padding-top: 42%; 499 | 500 | &-image { 501 | position: absolute; 502 | top: 0; 503 | left: 0; 504 | right: 0; 505 | bottom: 0; 506 | border-radius: var(--radius) var(--radius) 0 0; 507 | object-fit: cover; 508 | width: 100%; 509 | height: 100%; 510 | } 511 | } 512 | } 513 | } 514 | 515 | .jsd { 516 | &-list { 517 | &-main { 518 | header { 519 | padding: var(--su-2) 0 0 var(--su-2); 520 | @media (min-width: $break-small) { 521 | padding: 0; 522 | } 523 | } 524 | } 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | 3 | @mixin prefixOnly($property, $value, $prefixes, $important: false) { 4 | @each $prefix in $prefixes { 5 | @if $important { 6 | -#{$prefix}-#{$property}: $value !important; 7 | } @else { 8 | -#{$prefix}-#{$property}: $value; 9 | } 10 | } 11 | } 12 | 13 | @mixin prefix($property, $value, $prefixes, $important: false) { 14 | @include prefixOnly($property, $value, $prefixes); 15 | @if $important { 16 | #{$property}: $value !important; 17 | } @else { 18 | #{$property}: $value; 19 | } 20 | } 21 | 22 | @mixin borderBox() { 23 | @include prefix(box-sizing, border-box, $browsers); 24 | } 25 | 26 | @mixin borderRadius($value, $important: false) { 27 | @include prefix(border-radius, $value, $browsers, $important); 28 | } 29 | 30 | @mixin fixedTop($top, $left, $right) { 31 | position: fixed; 32 | top: $top; 33 | left: $left; 34 | right: $right; 35 | } 36 | 37 | @mixin clear() { 38 | display: table; 39 | clear: both; 40 | height: 0; 41 | content: ''; 42 | } 43 | 44 | @mixin clearBefore() { 45 | &:before { 46 | @include clear(); 47 | } 48 | } 49 | 50 | @mixin clearAfter() { 51 | &:after { 52 | @include clear(); 53 | } 54 | } 55 | 56 | @mixin clearBoth() { 57 | @include clearBefore(); 58 | @include clearAfter(); 59 | } 60 | 61 | @mixin zeroGap() { 62 | margin: 0; 63 | padding: 0; 64 | } 65 | 66 | @mixin zeroListGap() { 67 | @include zeroGap(); 68 | list-style-type: none; 69 | } 70 | 71 | @mixin boxShadow($offX, $offY, $blur, $spread, $colour) { 72 | @include prefix(box-shadow, #{$offX}px #{$offY}px #{$blur}px #{$spread}px #{$colour}, $browsers); 73 | } 74 | 75 | @mixin boxShadowInset($offX, $offY, $blur, $spread, $colour) { 76 | @include prefix(box-shadow, inset #{$offX}px #{$offY}px #{$blur}px #{$spread}px #{$colour}, $browsers); 77 | } 78 | 79 | @mixin boxShadowNoSpread($offX, $offY, $blur, $colour) { 80 | @include prefix(box-shadow, #{$offX}px #{$offY}px #{$blur}px #{$colour}, $browsers); 81 | } 82 | 83 | @mixin transition($opt, $time) { 84 | @include prefix(transition, #{$opt} #{$time}, $browsers); 85 | } 86 | 87 | @mixin listStyleNoneNoSpacing() { 88 | list-style-type: none; 89 | margin: 0; 90 | padding: 0; 91 | } 92 | 93 | @mixin fontSmoothing() { 94 | -webkit-font-smoothing: antialiased; 95 | -moz-osx-font-smoothing: grayscale; 96 | } 97 | 98 | @mixin nav_linkArrowIcon() { 99 | @include iconFont($colour_nav_link_hov, $icon_font); 100 | 101 | content: $icon_dn_arr; 102 | position: $icon_level0_pos; 103 | top: $icon_level0_top; 104 | right: $icon_level0_right; 105 | font-size: $icon_font_sz; 106 | } 107 | 108 | @mixin iconFont($color, $font) { 109 | color: $color; 110 | font-family: $font; 111 | font-style: normal; 112 | font-weight: normal; 113 | font-variant: normal; 114 | text-transform: none; 115 | line-height: 1; 116 | 117 | @include fontSmoothing(); 118 | } 119 | 120 | @mixin flexauto($auto) { 121 | flex: $auto; 122 | } 123 | 124 | @mixin flexbox($auto: false) { 125 | display: -webkit-box; 126 | display: -ms-flexbox; 127 | display: flex; 128 | 129 | @if $auto { 130 | @include flexauto($auto); 131 | } 132 | } 133 | 134 | @mixin flexCol() { 135 | @include flexbox(); 136 | flex-direction: column; 137 | } 138 | 139 | @mixin flexRow() { 140 | @include flexbox(); 141 | flex-direction: row; 142 | } 143 | 144 | @mixin opacity($value) { 145 | @include prefix(opacity, $value, browsers); 146 | filter: alpha(opacity=$value*100); 147 | } 148 | 149 | @mixin grid($gap: false, $tempCols: false) { 150 | display: grid; 151 | @if $gap { 152 | grid-gap: #{$gap}; 153 | } 154 | 155 | @if $tempCols { 156 | grid-template-columns: #{$tempCols}; 157 | } 158 | } 159 | 160 | @mixin shimmer() { 161 | background: #f6f7f8; 162 | background-image: linear-gradient(to right, rgba(255,255,255,0.1) 10%, rgba(0,0,0,0.05) 25%, rgba(255,255,255,0.1) 40%); 163 | background-repeat: no-repeat; 164 | background-size: 800px 104px; 165 | display: inline-block; 166 | position: relative; 167 | 168 | @include prefix(animation-duration, 1.25s, $browsers); 169 | @include prefix(animation-fill-mode, forwards, $browsers); 170 | @include prefix(animation-iteration-count, infinite, $browsers); 171 | @include prefix(animation-name, placeholderShimmer, $browsers); 172 | @include prefix(animation-timing-function, linear, $browsers); 173 | } 174 | -------------------------------------------------------------------------------- /src/scss/postroll.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | @import 'mixins'; 3 | 4 | :root { 5 | --card-bg: #{$colour_white}; 6 | --card-border: rgba(8, 9, 10, 0.1); 7 | } 8 | 9 | .jsd-story { 10 | @include boxShadow(0, 0, 0, 1, var(--card-border)); 11 | background: var(--card-bg); 12 | font-size: var(--fs-base); 13 | 14 | --story-padding: var(--su-3) var(--su-2) var(--su-2); 15 | --title-font-size: var(--fs-m); 16 | 17 | @media (min-width: $break-small) { 18 | border-radius: var(--border-radius); 19 | margin-bottom: var(--su-2); 20 | } 21 | 22 | @media (min-width: $break-tablet) { 23 | --story-padding: var(--su-4); 24 | } 25 | } 26 | 27 | .jsd-story-featured { 28 | @media (min-width: $break-tablet) { 29 | --title-font-size: var(--fs-3xl); 30 | } 31 | } 32 | 33 | .jsd-story-cover { 34 | display: block; 35 | width: 100%; 36 | height: auto; 37 | padding-bottom: 42%; 38 | background-size: cover; 39 | background-position: center center; 40 | 41 | @media (min-width: $break-small) { 42 | @include borderRadius(var(--border-radius) var(--border-radius) 0 0); 43 | } 44 | } 45 | 46 | .jsd-story-body { 47 | padding: var(--story-padding); 48 | } 49 | 50 | .jsd-story-top { 51 | @include flexbox(); 52 | align-items: center; 53 | justify-content: space-between; 54 | margin-bottom: var(--su-2); 55 | } 56 | 57 | .jsd-story-meta { 58 | @include flexbox(); 59 | align-items: center; 60 | line-height: var(--lh-tight); 61 | font-size: var(--fs-xs); 62 | 63 | time { 64 | font-size: inherit; 65 | color: inherit; 66 | font-weight: inherit; 67 | } 68 | } 69 | 70 | .jsd-story-author-pic { 71 | position: relative; 72 | margin-right: var(--su-2); 73 | } 74 | 75 | .jsd-avatar { 76 | display: inline-block; 77 | border-radius: 100%; 78 | position: relative; 79 | background-color: var(--card-color-tertiary); 80 | width: var(--su-6); 81 | height: var(--su-6); 82 | 83 | &::after { 84 | content: ''; 85 | border: 1px solid var(--body-color); 86 | opacity: 0.15; 87 | width: 100%; 88 | height: 100%; 89 | position: absolute; 90 | left: 0; 91 | top: 0; 92 | border-radius: 100%; 93 | pointer-events: none; 94 | } 95 | } 96 | 97 | .jsd-avatar-l { 98 | width: var(--su-7); 99 | height: var(--su-7); 100 | } 101 | 102 | .jsd-avatar-image { 103 | border-radius: 100%; 104 | width: 100%; 105 | height: 100%; 106 | display: inline-block; 107 | vertical-align: bottom; 108 | } 109 | 110 | .jsd-avatar-xl { 111 | width: var(--su-12); 112 | height: var(--su-12); 113 | } 114 | 115 | .jsd-avatar-2xl { 116 | width: var(--su-16); 117 | height: var(--su-16); 118 | } 119 | 120 | .jsd-story-secondary { 121 | color: var(--card-colour-secondary); 122 | 123 | &[href]:hover, &[enabled]:hover { 124 | color: var(--card-colour); 125 | } 126 | } 127 | 128 | .jsd-story-tertiary { 129 | color: var(--card-colour-tertiary); 130 | 131 | &[href]:hover, &[enabled]:hover { 132 | color: var(--card-colour); 133 | } 134 | } 135 | 136 | .jsd-story-title { 137 | color: var(--card-color); 138 | line-height: var(--lh-tight); 139 | margin-bottom: var(--su-2); 140 | font-size: var(--title-font-size); 141 | overflow-wrap: anywhere; 142 | word-break: break-word; 143 | 144 | a { 145 | color: inherit; 146 | display: block; 147 | } 148 | } 149 | 150 | .jsd-tag { 151 | font-size: var(--fs-xxs); 152 | color: var(--tag-colour); 153 | line-height: 1.3; 154 | border-radius: var(--border-radius); 155 | padding: var(--su-1); 156 | display: inline-block; 157 | 158 | @media (min-width: $break-tablet) { 159 | font-size: var(--fs-xs); 160 | } 161 | 162 | &[href]:hover { 163 | color: var(--tag-colour-hover); 164 | } 165 | } 166 | 167 | .jsd-tag-prefix { 168 | opacity: 0.4; 169 | font-weight: normal; 170 | } 171 | 172 | .jsd-scaffold { 173 | border-radius: var(--border-radius); 174 | background: var(--body-color); 175 | opacity: 0.025; 176 | } 177 | 178 | .jsd-scaffold-loading { 179 | @include borderRadius(var(--border-radius)); 180 | @include boxShadowInset(0, 0, 0, 200, rgba(0,0,0,0.025)); 181 | } 182 | 183 | .jsd-story-indentation { 184 | padding-left: calc( var(--su-7) + var(--su-2)); 185 | } 186 | 187 | .jsd-layout-postroll-rooter { 188 | @include flexbox(); 189 | 190 | ul,ol,li { 191 | list-style: none; 192 | margin: 0; 193 | padding: 0; 194 | } 195 | 196 | ul, ol { 197 | width: 100%; 198 | 199 | @media (min-width: $break-tablet) { 200 | @include flexRow(); 201 | flex-wrap: nowrap; 202 | justify-content: space-between; 203 | } 204 | } 205 | 206 | &.item-prev { 207 | @media (min-width: $break-tablet) { 208 | ul, ol { 209 | justify-content: flex-start; 210 | } 211 | } 212 | 213 | &.item-next { 214 | @media (min-width: $break-tablet) { 215 | ul, ol { 216 | justify-content: space-between; 217 | } 218 | } 219 | } 220 | } 221 | 222 | &.item-next{ 223 | &:not(.item-prev) { 224 | @media (min-width: $break-tablet) { 225 | ul, ol { 226 | justify-content: flex-end; 227 | } 228 | } 229 | } 230 | } 231 | 232 | li { 233 | @media (min-width: $break-tablet) { 234 | flex-basis: auto; 235 | } 236 | 237 | a { 238 | @include flexRow(); 239 | justify-content: center; 240 | align-items: center; 241 | padding: var(--su-4) var(--su-8); 242 | } 243 | } 244 | } 245 | 246 | .jsd-postroll-footer-details { 247 | @include flexCol(); 248 | } 249 | 250 | .jsd-postroll-footer-link { 251 | min-width: 100%; 252 | 253 | @media (min-width: $break-tablet) { 254 | min-width: 45%; 255 | width: 45%; 256 | } 257 | 258 | @media (min-width: $break-medium) { 259 | min-width: 35%; 260 | width: 35%; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/scss/syntax.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --syntax-background-colour: #08090a; 3 | --syntax-keyword-colour: rgb(14, 167, 233); 4 | --syntax-text-colour: #f8f8f2; 5 | --syntax-comment-colour: #808080; 6 | --syntax-declaration-colour: #f39c12; 7 | --syntax-literal-colour: #dda0dd; 8 | --syntax-error-colour: #f9690e; 9 | --syntax-name-colour: #7ed07e; 10 | --syntax-string-colour: #f2ca27; 11 | --syntax-function-colour: #DD4A68; 12 | --syntax-inline-colour: #d81111; 13 | } 14 | 15 | .gatsby-highlight { 16 | @include borderRadius(var(--border-radius)); 17 | font-family: var(--ff-console); 18 | font-size: var(--fs-s); 19 | 20 | background: var(--syntax-background-colour); 21 | color: var(--syntax-text-colour); 22 | padding: var(--su-1); 23 | margin: 0; 24 | white-space: pre; 25 | overflow-x: auto; 26 | -webkit-overflow-scrolling: touch; 27 | overflow-wrap: initial; 28 | 29 | @media (min-width: $break-small) { 30 | padding: var(--su-3) var(--su-6); 31 | padding: var(--su-2); 32 | } 33 | 34 | pre { 35 | background: var(--syntax-background-colour); 36 | color: var(--syntax-text-colour); 37 | padding: var(--su-1); 38 | margin: 0; 39 | white-space: pre; 40 | overflow-x: auto; 41 | -webkit-overflow-scrolling: touch; 42 | overflow-wrap: initial; 43 | 44 | @media (min-width: $break-small) { 45 | padding: var(--su-2); 46 | } 47 | 48 | code { 49 | background: rgba(0,0,0,0.15); 50 | color: var(--color-body-color); 51 | border-radius: var(--radius); 52 | max-width: 100%; 53 | font-size: var(--fs-xs); 54 | 55 | * { 56 | font-size: var(--fs-xs); 57 | } 58 | } 59 | } 60 | 61 | code[class*="language-"], pre[class*="language-"] { 62 | background: var(--syntax-background-colour); 63 | color: var(--syntax-text-colour); 64 | } 65 | 66 | .token.atrule, .token.attr-value, .token.keyword { 67 | color: var(--syntax-keyword-colour); 68 | } 69 | 70 | .token.function, .token.class-name { 71 | color: var(--syntax-function-colour); 72 | } 73 | 74 | .token.function-variable { 75 | color: var(--syntax-declaration-colour); 76 | } 77 | 78 | .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { 79 | color: var(--syntax-text-colour); 80 | background: transparent; 81 | } 82 | 83 | .token.punctuation { 84 | color: var(--syntax-text-colour); 85 | } 86 | 87 | .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { 88 | color: var(--syntax-string-colour); 89 | } 90 | 91 | .token.comment, .token.prolog, .token.doctype, .token.cdata { 92 | color: var(--syntax-comment-colour); 93 | } 94 | 95 | .token.template-punctuation { 96 | color: var(--syntax-literal-colour); 97 | } 98 | 99 | .token.template-punctuation + .token.string { 100 | color: var(--syntax-literal-colour); 101 | } 102 | 103 | .token.boolean { 104 | color: var(--syntax-keyword-colour); 105 | } 106 | 107 | .token.interpolation-punctuation { 108 | color: var(--syntax-keyword-colour); 109 | } 110 | 111 | .token.tag { 112 | color: var(--syntax-declaration-colour); 113 | 114 | .token.punctuation { 115 | color: var(--syntax-text-colour); 116 | } 117 | } 118 | } 119 | 120 | code.language-text { 121 | @include borderRadius(3px); 122 | font-family: var(--ff-monospace); 123 | background: var(--base-10); 124 | color: var(--syntax-inline-colour); 125 | font-size: calc(var(--text-style-fs) * 0.9); 126 | } -------------------------------------------------------------------------------- /src/scss/table.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | @import 'mixins'; 3 | @import 'base'; 4 | 5 | .jsd { 6 | &-article { 7 | &-body { 8 | table { 9 | border: 1px solid var(--base-20); 10 | border-spacing: 0; 11 | border-collapse: collapse; 12 | 13 | th, td { 14 | border: 1px solid var(--base-20); 15 | padding: var(--su-2) var(--su-4); 16 | background: var(--base-inverted); 17 | } 18 | 19 | thead { 20 | th { 21 | background: var(--base-0); 22 | font-weight: var(--fw-heavy); 23 | font-size: var(--fs-s); 24 | } 25 | } 26 | 27 | tbody { 28 | td { 29 | font-size: var(--fs-xs); 30 | font-family: var(--ff-monospace); 31 | 32 | code { 33 | font-size: inherit; 34 | } 35 | } 36 | 37 | tr { 38 | &:nth-child(even) { 39 | td { 40 | background: var(--base-0); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/scss/vars.scss: -------------------------------------------------------------------------------- 1 | // Defaults 2 | $browsers: moz webkit; 3 | 4 | // Breaks 5 | $break-large: 1280px; 6 | $break-medium: 1024px; 7 | $break-small: 768px; 8 | $break-tablet: 640px; 9 | $break-mobile: 460px; 10 | 11 | // Displays 12 | $disp_block_imp: block !important; 13 | $disp_block: block; 14 | $disp_none_imp: none !important; 15 | $disp_none: none; 16 | 17 | // Font weights 18 | $fnt_wt_light: 300; 19 | $fnt_wt_normal: 400; 20 | $fnt_wt_med: 500; 21 | $fnt_wt_bold: 600; 22 | $fnt_wt_bolder: bold; 23 | 24 | // Font sizes 25 | $fnt-sz-xl: 2.25rem; 26 | $fnt-sz-l: 1.75rem; 27 | $fnt-sz-3xl: 1.85rem; 28 | $fnt-sz-2xl: 1.5rem; 29 | $fnt-sz-m: 1.25rem; 30 | $fnt-sz-s: 1.125rem; 31 | $fnt-sz-base: 1rem; 32 | $fnt-sz-xs: 0.85rem; 33 | $fnt-sz-xxs: 0.75rem; 34 | 35 | // Body 36 | $def_font: 'Source Sans Pro', sans-serif; 37 | $def_font_size: $fnt-sz-s; 38 | $def_font_weight: $fnt_wt_normal; 39 | 40 | // Colours 41 | $colour_white: #fff; 42 | $colour_beige: #f9fafa; 43 | $colour_black: #000; 44 | $colour_dark_navy_blue: #08090a; 45 | $colour_dark_grey_link: #202428; 46 | $colour_dark_grey: #333447; 47 | $colour_dark_gray: #333447; 48 | $colour_navy_blue: #1e2427; 49 | $colour_dark_blue: #193c9d; 50 | $colour_orange: #f9b109; 51 | $colour_orange_trans80: rgba(249, 177, 9, 0.8); 52 | $colour_orange_trans90: rgba(249, 177, 9, 0.9); 53 | $colour_dark_grey: #444444; 54 | $colour_body_grey: #f2f3f4; 55 | $colour_yellow: #FDE000; 56 | $colour_card: $colour_dark_navy_blue; 57 | $colour_card_tertiary: #64707d; 58 | $colour_card_secondary: #4d5760; 59 | $colour_tag: $colour_card_tertiary; 60 | $colour_tag_hover: $colour_dark_navy_blue; 61 | 62 | // Links 63 | $link_colour: $colour_dark_grey_link; 64 | $link_colour_bg: rgba(8, 9, 10, 0.05); 65 | $link_colour_hover: blue; 66 | 67 | // Header 68 | $header_zindex: 110; 69 | $header_height: 56px; 70 | $header_bgcolour: $colour_white; 71 | $header_fnt_sm: 0.875em; 72 | $header_logo_max_height: 40px; 73 | $header_logo_max_width: 80px; 74 | 75 | // Layout 76 | $grid_layout: 240px 2fr 1fr; 77 | $grid_layout_padding: 1rem; 78 | $grid_layout_gap: 1rem; 79 | 80 | -------------------------------------------------------------------------------- /src/templates/author.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | import LayoutAuthor from "../components/layoutAuthor" 4 | import PostRollByAuthorId from "../components/postRollByAuthorId" 5 | import SEO from "../components/seo" 6 | import useGetAuthorDetailsStatic from "../hooks/static/getAuthorDetailsStatic" 7 | import RenderSidebarAuthorDetails from "../components/sidebar/renderSidebarAuthorDetails" 8 | 9 | const AuthorTemplate = ({ pageContext, data, location }) => { 10 | const { author } = pageContext; 11 | const siteTitle = data.site.siteMetadata?.title || `Title` 12 | const authorDetails = useGetAuthorDetailsStatic(author); 13 | 14 | return ( 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 |

Posts

24 |
25 |
26 | 27 |
28 |
29 | ) 30 | } 31 | 32 | export default AuthorTemplate; 33 | 34 | export const pageQuery = graphql` 35 | query { 36 | site { 37 | siteMetadata { 38 | title 39 | } 40 | } 41 | } 42 | ` 43 | -------------------------------------------------------------------------------- /src/templates/blog-post.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | import SEO from "../components/seo" 4 | import LayoutPost from "../components/layoutPost" 5 | import PostRollFooter from "../components/layout/postRollFooter" 6 | import PostTitleHeader from "../components/article/postTitleHeader" 7 | import RenderArticleMain from "../components/article/renderMainArticle" 8 | import { RenderComments } from "../components/article/renderComments"; 9 | import { RenderAuthorDetails } from "../components/article/renderAuthorDetails"; 10 | import useGetAuthorDetailsStatic from "../hooks/static/getAuthorDetailsStatic"; 11 | 12 | const BlogPostTemplate = ({ data, pageContext, location }) => { 13 | const post = data.allMarkdownRemark.nodes[0]; 14 | const siteTitle = data.site.siteMetadata?.title || `Title` 15 | const { previous, next } = pageContext 16 | 17 | const seoProps = { 18 | title: post.frontmatter.title, 19 | description: post.frontmatter.description || post.excerpt, 20 | twitterCreator: pageContext?.authors[0] || undefined, 21 | location, 22 | heroImage: post.frontmatter?.hero?.childImageSharp?.fluid?.src, 23 | twitterDescription: post.excerpt 24 | }; 25 | 26 | const authorDetails = useGetAuthorDetailsStatic(post.frontmatter.authors); 27 | 28 | return ( 29 | 30 | 31 |
32 |
33 |
34 | 35 | 36 |
37 | 38 | 39 |
40 | 41 |
42 |
43 | ) 44 | } 45 | 46 | export default BlogPostTemplate 47 | 48 | export const pageQuery = graphql` 49 | query BlogPostBySlug($slug: String!) { 50 | site { 51 | siteMetadata { 52 | title 53 | } 54 | } 55 | allMarkdownRemark(filter: {fields: {slug: {eq: $slug}}}, limit: 1) { 56 | nodes { 57 | id 58 | timeToRead 59 | fields { 60 | slug 61 | } 62 | html 63 | excerpt(pruneLength: 125, format: PLAIN) 64 | frontmatter { 65 | date(formatString: "MMMM DD, YYYY") 66 | publishedDate(formatString: "MMMM DD, YYYY") 67 | updatedDate(formatString: "MMMM DD, YYYY") 68 | published 69 | title 70 | authors 71 | posttags 72 | hero { 73 | childImageSharp { 74 | fluid(jpegQuality: 80, maxWidth: 1280) { 75 | src 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | ` 84 | -------------------------------------------------------------------------------- /src/templates/tag.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | import Layout from "../components/layout" 4 | import SEO from "../components/seo" 5 | import PostRollByTagName from "../components/postRollByTagName"; 6 | import useGetPostTagDetailsStatic from "../hooks/static/getPostTagDetailsStatic"; 7 | import SidebarTagDetails from "../components/sidebar/sidebarTagDetails"; 8 | 9 | const TagTemplate = ({ pageContext, data, location }) => { 10 | const { tag } = pageContext; 11 | const siteTitle = data.site.siteMetadata?.title || `Title` 12 | const tagDetails = useGetPostTagDetailsStatic(tag); 13 | 14 | return ( 15 | 16 | 17 |
18 | 19 |
20 | 21 |
22 |

Posts

23 |
24 |
25 | 26 |
27 |
28 | ) 29 | } 30 | 31 | export default TagTemplate; 32 | 33 | export const pageQuery = graphql` 34 | query { 35 | site { 36 | siteMetadata { 37 | title 38 | } 39 | } 40 | } 41 | ` 42 | -------------------------------------------------------------------------------- /src/utils/className.js: -------------------------------------------------------------------------------- 1 | export const className = (options) => { 2 | const classList = []; 3 | if (Object(options) === options) { 4 | Object.keys(options).forEach(key => { 5 | if (!!options[key]) { 6 | classList.push(key); 7 | } 8 | }); 9 | } 10 | 11 | return classList.join(' '); 12 | } -------------------------------------------------------------------------------- /src/utils/customSlug.js: -------------------------------------------------------------------------------- 1 | //const { getSlugDate } = require("./date"); 2 | 3 | exports.generateCustomPostSlug = ({ id, frontmatter, fields, published = true}) => { 4 | const { slug } = fields; 5 | const { authors } = frontmatter; 6 | const prefix = published ? "" : "draft-"; 7 | 8 | return `/${authors[0]}/${prefix}${slug.replace(/\//g,"")}-${id.substr(id.length-3, 3)}`; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/date.js: -------------------------------------------------------------------------------- 1 | const getIsoFormatDate = (date) => { 2 | return `${date.getFullYear()}-${(date.getMonth()+"").padStart(2, "0")}-${(date.getDate()+"").padStart(2, "0")}`; 3 | } 4 | 5 | const getSlugDate = (date) => { 6 | return getIsoFormatDate(date).replace(/-/g, ''); 7 | } 8 | 9 | exports = { 10 | getIsoFormatDate, 11 | getSlugDate 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/mapPostsToTags.js: -------------------------------------------------------------------------------- 1 | export const MapPostsToTags = (posts) => { 2 | const tags = {}; 3 | posts.forEach(post => { 4 | const tag = post.tags[0]; 5 | if (!tags.hasOwnProperty(tag)) { 6 | tags[tag] = { 7 | posts: [] 8 | } 9 | } 10 | 11 | tags[tag].posts.push(post); 12 | }); 13 | 14 | return { 15 | map: () => tags, 16 | keys: () => Object.keys(tags) 17 | } 18 | } -------------------------------------------------------------------------------- /src/utils/mapTagDetailsToPostsTagsMap.js: -------------------------------------------------------------------------------- 1 | export const MapTagDetailstoPostsTagsMap = (tagsMap, tagDetails) => { 2 | const map = tagsMap.map(); 3 | const keys = tagsMap.keys(); 4 | 5 | tagDetails 6 | .all() 7 | .forEach(tag => { 8 | if (keys.includes(tag.id)) { 9 | map[tag.id].details = tag; 10 | } 11 | }) 12 | 13 | return { 14 | toArray: () => tagsMap.keys().map(key => map[key]), 15 | data: { ...tagsMap.map() } 16 | }; 17 | } -------------------------------------------------------------------------------- /static/CNAME: -------------------------------------------------------------------------------- 1 | thejs.dev -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thejsdevsite/jsdev/ca597d5ef310f275843f2acf05809cebf5c8101d/static/favicon.ico -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | 4 | Sitemap: https://thejs.dev/sitemap.xml -------------------------------------------------------------------------------- /uid.js: -------------------------------------------------------------------------------- 1 | const uniqid = require('uniqid'); 2 | console.log(uniqid.process()); --------------------------------------------------------------------------------