├── .browserslistrc ├── .editorconfig ├── .env.example ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── content ├── default-og.jpg └── settings.json ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── lambda ├── src │ ├── contact.js │ └── instagram.js └── webpack.config.js ├── netlify.toml ├── package.json ├── plugins ├── gatsby-plugin-nprogress │ ├── gatsby-browser.js │ ├── index.js │ └── package.json ├── gatsby-remark-image-component │ ├── index.js │ └── package.json └── rehype-wrap-in-columns.js ├── readme.md ├── renovate.json ├── requirements.txt ├── src ├── assets │ ├── fonts │ │ ├── Lineto EULA.pdf │ │ ├── circular-black.woff │ │ ├── circular-black.woff2 │ │ ├── circular-bold.woff │ │ ├── circular-bold.woff2 │ │ ├── circular-bolditalic.woff │ │ ├── circular-bolditalic.woff2 │ │ ├── circular-book.woff │ │ ├── circular-book.woff2 │ │ ├── circular-bookitalic.woff │ │ ├── circular-bookitalic.woff2 │ │ ├── circular-medium.woff │ │ ├── circular-medium.woff2 │ │ └── circular.css │ └── icon.png ├── components │ ├── App.js │ ├── Awards.js │ ├── Box.js │ ├── BoxSection.js │ ├── Button.js │ ├── Card.js │ ├── CardInfo.js │ ├── CleanTag.js │ ├── ContentWrapper.js │ ├── CookieToast.js │ ├── Cover.js │ ├── Dialog.js │ ├── Div.js │ ├── EmbedPlayer.js │ ├── Footer.js │ ├── FrontH1.js │ ├── Grid.js │ ├── Header.js │ ├── Hero.js │ ├── Icon.js │ ├── IdManager.js │ ├── Image.js │ ├── ImageLogo.js │ ├── InstagramFeed.js │ ├── Layout.js │ ├── Link.js │ ├── List.js │ ├── Logo.js │ ├── LogoIcon.js │ ├── MDXComponents.js │ ├── Meta.js │ ├── Nav.js │ ├── NoSSR.js │ ├── Playground │ │ ├── Playground.js │ │ ├── glyphs │ │ │ ├── a.svg │ │ │ ├── e.svg │ │ │ ├── g.svg │ │ │ ├── index.js │ │ │ ├── play.svg │ │ │ ├── r.svg │ │ │ ├── s.svg │ │ │ └── t.svg │ │ └── index.js │ ├── Portal.js │ ├── RichTextContentful.js │ ├── Section.js │ ├── SelectLanguage.js │ ├── SkipToContentLink.js │ ├── Tags.js │ ├── Text.js │ ├── TextField.js │ ├── Tile.js │ └── Video.js ├── context │ └── ThemeContext.js ├── lib │ ├── format.js │ ├── getMetaFromPost.js │ ├── iconLibrary.js │ ├── useAxios.js │ ├── useDisableScroll.js │ ├── useFocusTrap.js │ ├── useForceUpdate.js │ ├── useFormin.js │ ├── useIntersectionObserver.js │ ├── useMeasure.js │ ├── useSiteSettings.js │ └── useToggle.js ├── routes.js ├── style.js └── templates │ ├── 404.js │ ├── about.js │ ├── career.js │ ├── case.js │ ├── cases.js │ ├── contact.js │ ├── frontpage.js │ ├── position.js │ ├── post.js │ ├── posts.js │ └── standard.js ├── static ├── AB_StrategAgency_01.png ├── AB_StrategAgency_02.png ├── Vinnarknapp_ÅB.png ├── Vinnarknapp_ÅB_Eng.png ├── _redirects ├── admin │ └── config.yml ├── favicon.ico ├── robots.txt ├── waveDark.svg └── waveLight.svg └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | > 5% in SE 4 | last 2 firefox versions 5 | last 2 chrome versions 6 | last 2 edge versions 7 | last 2 iOS versions 8 | last 2 safari versions 9 | ie 11 10 | not dead -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | HUBSPOT_API_KEY= 2 | HUBSPOT_PORTAL_ID= 3 | INSTAGRAM_ACCESS_TOKEN= 4 | SENDGRID_API_KEY= 5 | CONTENTFUL_ACCESS_TOKEN= 6 | CONTENTFUL_ACCESS_TOKEN_PREVIEW= 7 | CONTENTFUL_ENVIRONMENT=master 8 | GOOGLE_TAGMANAGER_ID= 9 | GOOGLE_ANALYTICS_ID= -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "strateg", 3 | "rules": { 4 | "jsx-a11y/anchor-is-valid": "off", 5 | "react/require-default-props": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | 6 | # Build 7 | public 8 | lambda/dist 9 | .cache 10 | 11 | # Misc 12 | .DS_Store 13 | .vscode 14 | *.orig 15 | .env 16 | yarn-error.log 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @fortawesome:registry=https://npm.fontawesome.com/ 2 | //npm.fontawesome.com/:_authToken=${FONTAWESOME_TOKEN} -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | .cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-strateg/css-in-js", 3 | "rules": { 4 | "scss/at-rule-no-unknown": null 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /content/default-og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/content/default-og.jpg -------------------------------------------------------------------------------- /content/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Strateg Agency", 3 | "shortName": "Strateg", 4 | "offices": [ 5 | { 6 | "address": "Nybrokajen 5, 5 tr", 7 | "zipcode": "111 48", 8 | "city": "Stockholm", 9 | "email": "hello@strateg.se", 10 | "phone": "+46 8 588 095 00" 11 | }, 12 | { 13 | "address": "Slöjdgatan 39", 14 | "zipcode": "703 83", 15 | "city": "Örebro", 16 | "email": "hello@strateg.se", 17 | "phone": "+46 8 588 095 00" 18 | } 19 | ], 20 | "social": { 21 | "facebook": "https://www.facebook.com/strategagency/", 22 | "facebook_app_id": "261878794741046", 23 | "instagram": "https://www.instagram.com/strategagency/", 24 | "linkedin": "https://www.linkedin.com/company/strateg-marknadsf-ring-ab", 25 | "github": "https://github.com/strt" 26 | }, 27 | "seo": { 28 | "default_image": "./default-og.jpg" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Cookie from 'js-cookie' 3 | import * as Sentry from '@sentry/browser' 4 | import { ThemeProvider } from './src/context/ThemeContext' 5 | 6 | require('focus-visible') 7 | 8 | export const wrapRootElement = ({ element }) => ( 9 | {element} 10 | ) 11 | 12 | Sentry.init({ 13 | dsn: process.env.SENTRY_KEY, 14 | }) 15 | 16 | export const onClientEntry = () => { 17 | window[`ga-disable-${process.env.GATSBY_GOOGLE_ANALYTICS_ID}`] = true 18 | 19 | if ( 20 | Cookie.get('accept_cookies') === 'true' && 21 | Cookie.get('accept_analytics') === 'true' 22 | ) { 23 | window[`ga-disable-${process.env.GATSBY_GOOGLE_ANALYTICS_ID}`] = false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, global-require */ 2 | const proxy = require('http-proxy-middleware') 3 | require('dotenv').config() 4 | 5 | const siteUrl = 6 | process.env.ACTIVE_ENV === 'staging' 7 | ? 'https://strateg.dev' 8 | : 'https://strateg.se' 9 | 10 | module.exports = { 11 | siteMetadata: { 12 | siteUrl, 13 | }, 14 | plugins: [ 15 | { 16 | resolve: 'gatsby-plugin-robots-txt', 17 | options: { 18 | host: siteUrl, 19 | sitemap: `${siteUrl}/sitemap.xml`, 20 | resolveEnv: () => process.env.ACTIVE_ENV, 21 | env: { 22 | staging: { 23 | policy: [{ userAgent: '*', disallow: ['/'] }], 24 | }, 25 | production: { 26 | policy: [{ userAgent: '*', allow: '/' }], 27 | }, 28 | }, 29 | }, 30 | }, 31 | { 32 | resolve: 'gatsby-source-filesystem', 33 | options: { 34 | path: `${__dirname}/content`, 35 | name: 'content', 36 | }, 37 | }, 38 | 'gatsby-plugin-sharp', 39 | 'gatsby-transformer-sharp', 40 | 'gatsby-transformer-json', 41 | { 42 | resolve: 'gatsby-mdx', 43 | options: { 44 | extensions: ['.mdx', '.md'], 45 | commonmark: true, 46 | rehypePlugins: [require('./plugins/rehype-wrap-in-columns')], 47 | remarkPlugins: [ 48 | require('remark-unwrap-images'), 49 | require('remark-external-links'), 50 | ], 51 | gatsbyRemarkPlugins: [ 52 | { 53 | resolve: 'gatsby-remark-relative-images', 54 | options: { 55 | name: 'media', 56 | }, 57 | }, 58 | { 59 | resolve: require.resolve('./plugins/gatsby-remark-image-component'), 60 | }, 61 | ], 62 | }, 63 | }, 64 | { 65 | resolve: 'gatsby-plugin-layout', 66 | options: { 67 | component: require.resolve('./src/components/App.js'), 68 | }, 69 | }, 70 | 'gatsby-plugin-sitemap', 71 | { 72 | resolve: 'gatsby-plugin-google-analytics', 73 | options: { 74 | trackingId: process.env.GATSBY_GOOGLE_ANALYTICS_ID, 75 | anonymize: true, 76 | respectDNT: true, 77 | }, 78 | }, 79 | { 80 | resolve: 'gatsby-plugin-google-tagmanager', 81 | options: { 82 | id: process.env.GOOGLE_TAGMANAGER_ID, 83 | includeInDevelopment: true, 84 | }, 85 | }, 86 | { 87 | resolve: 'gatsby-plugin-manifest', 88 | options: { 89 | name: 'Strateg Agency', 90 | short_name: 'Strateg', 91 | start_url: '.', 92 | background_color: '#ffffff', 93 | theme_color: '#0b101e', 94 | display: 'fullscreen', 95 | icon: 'src/assets/icon.png', 96 | }, 97 | }, 98 | // 'gatsby-plugin-subfont', Enable again once it's less buggy 99 | 'gatsby-plugin-catch-links', 100 | { 101 | resolve: `gatsby-plugin-nprogress`, 102 | options: { 103 | showSpinner: false, 104 | }, 105 | }, 106 | 'gatsby-plugin-react-helmet', 107 | 'gatsby-plugin-styled-components', 108 | 'gatsby-plugin-netlify', 109 | 'gatsby-plugin-netlify-cache', 110 | { 111 | resolve: `gatsby-source-contentful`, 112 | options: { 113 | spaceId: `lxxyo1cefolk`, 114 | // Learn about environment variables: https://gatsby.dev/env-vars 115 | environment: process.env.CONTENTFUL_ENVIRONMENT, 116 | accessToken: 117 | process.env.ACTIVE_ENV === 'staging' 118 | ? process.env.CONTENTFUL_ACCESS_TOKEN 119 | : process.env.CONTENTFUL_ACCESS_TOKEN, 120 | host: 121 | process.env.ACTIVE_ENV === 'staging' 122 | ? 'cdn.contentful.com' 123 | : 'cdn.contentful.com', 124 | }, 125 | }, 126 | 'gatsby-plugin-client-side-redirect', 127 | `gatsby-plugin-meta-redirect`, 128 | ], 129 | developMiddleware: app => { 130 | app.use( 131 | '/.netlify/functions/', 132 | proxy({ 133 | target: 'http://localhost:9000', 134 | pathRewrite: { 135 | '/.netlify/functions/': '', 136 | }, 137 | }), 138 | ) 139 | }, 140 | } 141 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const { fmImagesToRelative } = require('gatsby-remark-relative-images') 3 | 4 | function getLangOptions(node) { 5 | const localePath = node.node_locale === 'sv' ? '' : `/en` 6 | const slug = node.slug === '/' ? '/' : `/${node.slug}/` 7 | 8 | return { 9 | localePath, 10 | slug, 11 | } 12 | } 13 | 14 | exports.createPages = async ({ actions, graphql }) => { 15 | const { createPage, createRedirect } = actions 16 | 17 | // ============= PAGES ============= 18 | 19 | const contentfulPages = await graphql(` 20 | { 21 | allContentfulPages { 22 | edges { 23 | node { 24 | name 25 | title 26 | template 27 | node_locale 28 | slug 29 | alias 30 | } 31 | } 32 | } 33 | } 34 | `) 35 | 36 | contentfulPages.data.allContentfulPages.edges.forEach(({ node }) => { 37 | const { slug, localePath } = getLangOptions(node) 38 | 39 | const template = node.template ? node.template.toLowerCase() : 'standard' 40 | const fullpath = `${localePath}${slug}` 41 | 42 | createPage({ 43 | path: fullpath, 44 | component: resolve(`./src/templates/${template}.js`), 45 | context: { 46 | locale: node.node_locale, 47 | slug: node.slug, 48 | }, 49 | }) 50 | 51 | if (node.alias) { 52 | node.alias.forEach(alias => { 53 | createRedirect({ 54 | fromPath: `${localePath}/${alias}/`, 55 | toPath: fullpath, 56 | statusCode: 200, 57 | }) 58 | }) 59 | } 60 | }) 61 | 62 | // ============= CASES ============= 63 | 64 | const contentfulCases = await graphql(` 65 | { 66 | allContentfulCase { 67 | edges { 68 | node { 69 | slug 70 | title 71 | node_locale 72 | alias 73 | } 74 | } 75 | } 76 | } 77 | `) 78 | 79 | contentfulCases.data.allContentfulCase.edges.forEach(({ node }) => { 80 | const { slug, localePath } = getLangOptions(node) 81 | const path = localePath === '' ? 'case' : 'work' 82 | const fullpath = `${localePath}/${path}${slug}` 83 | 84 | createPage({ 85 | path: fullpath, 86 | component: resolve(`./src/templates/case.js`), 87 | context: { 88 | slug: node.slug, 89 | locale: node.node_locale, 90 | }, 91 | }) 92 | 93 | if (node.alias) { 94 | node.alias.forEach(alias => { 95 | createRedirect({ 96 | fromPath: `${localePath}/${alias}/`, 97 | toPath: fullpath, 98 | statusCode: 200, 99 | }) 100 | }) 101 | } 102 | }) 103 | 104 | // ============= POSITIONS ============= 105 | 106 | const contentfulPositions = await graphql(` 107 | { 108 | allContentfulPositions { 109 | edges { 110 | node { 111 | slug 112 | title 113 | node_locale 114 | alias 115 | } 116 | } 117 | } 118 | } 119 | `) 120 | 121 | contentfulPositions.data.allContentfulPositions.edges.forEach(({ node }) => { 122 | const { slug, localePath } = getLangOptions(node) 123 | const path = localePath === '' ? 'bli-en-av-oss' : 'join-us' 124 | const fullpath = `${localePath}/${path}${slug}` 125 | 126 | if (node.slug !== 'dummy') { 127 | createPage({ 128 | path: fullpath, 129 | component: resolve(`./src/templates/position.js`), 130 | context: { 131 | slug: node.slug, 132 | locale: node.node_locale, 133 | }, 134 | }) 135 | } 136 | 137 | if (node.alias) { 138 | node.alias.forEach(alias => { 139 | createRedirect({ 140 | fromPath: `${localePath}/${alias}/`, 141 | toPath: fullpath, 142 | statusCode: 200, 143 | }) 144 | }) 145 | } 146 | }) 147 | 148 | // ============= POSTS ============= 149 | 150 | const contentfulPosts = await graphql(` 151 | { 152 | allContentfulPosts { 153 | edges { 154 | node { 155 | slug 156 | title 157 | alias 158 | node_locale 159 | } 160 | } 161 | } 162 | } 163 | `) 164 | 165 | const posts = contentfulPosts.data.allContentfulPosts.edges 166 | posts.forEach(({ node }) => { 167 | const { slug, localePath } = getLangOptions(node) 168 | const path = localePath === '' ? 'aktuellt' : 'news' 169 | const fullpath = `${localePath}/${path}${slug}` 170 | 171 | createPage({ 172 | path: fullpath, 173 | component: resolve(`./src/templates/post.js`), 174 | context: { 175 | slug: node.slug, 176 | locale: node.node_locale, 177 | }, 178 | }) 179 | 180 | if (node.alias) { 181 | node.alias.forEach(alias => { 182 | createRedirect({ 183 | fromPath: `${localePath}/${alias}/`, 184 | toPath: fullpath, 185 | statusCode: 200, 186 | }) 187 | }) 188 | } 189 | }) 190 | } 191 | 192 | exports.onCreateNode = ({ node }) => { 193 | fmImagesToRelative(node) 194 | } 195 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ThemeProvider } from './src/context/ThemeContext' 3 | 4 | require('focus-visible') 5 | 6 | export const wrapRootElement = ({ element }) => ( 7 | {element} 8 | ) 9 | -------------------------------------------------------------------------------- /lambda/src/contact.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string' 2 | import sendgrid from '@sendgrid/mail' 3 | 4 | sendgrid.setApiKey(process.env.SENDGRID_API_KEY) 5 | 6 | function parseBody(event) { 7 | const contentType = event.headers['content-type'] 8 | 9 | if (contentType === 'application/x-www-form-urlencoded') { 10 | return queryString.parse(event.body) 11 | } 12 | 13 | if (contentType.includes('application/json')) { 14 | return JSON.parse(event.body) 15 | } 16 | 17 | return {} 18 | } 19 | 20 | function capitalize(str) { 21 | return str.charAt(0).toUpperCase() + str.slice(1) 22 | } 23 | 24 | export async function handler(event) { 25 | const method = event.httpMethod 26 | const path = event.path 27 | .replace('/.netlify/functions', '') 28 | .split('/') 29 | .filter(Boolean) 30 | const [, form] = path 31 | const body = parseBody(event) 32 | 33 | try { 34 | if (form === 'career') { 35 | if (method !== 'POST') { 36 | return { 37 | statusCode: 405, 38 | body: '405 Method not allowed', 39 | } 40 | } 41 | 42 | let html = `

"Career" form submission


` 43 | html = Object.entries(body).reduce((acc, [key, value]) => { 44 | return `${acc}${capitalize(key)}
${value}

` 45 | }, html) 46 | 47 | const msg = { 48 | to: 'alexander.nanberg@strateg.se', 49 | from: 'no-reply@strateg.se', 50 | subject: 'Contact form – strateg.se', 51 | html, 52 | } 53 | 54 | await sendgrid.send(msg) 55 | 56 | return { 57 | statusCode: 200, 58 | headers: { 59 | 'content-type': 'application/json', 60 | }, 61 | body: JSON.stringify({ message: 'Message sent' }), 62 | } 63 | } 64 | } catch (e) { 65 | if (e.response) { 66 | return { 67 | statusCode: e.response.status, 68 | headers: { 69 | 'content-type': 'application/json', 70 | }, 71 | body: JSON.stringify(e.response.data), 72 | } 73 | } 74 | 75 | console.log(e) 76 | 77 | return { 78 | statusCode: 500, 79 | body: '500 Internal server error', 80 | } 81 | } 82 | 83 | return { 84 | statusCode: 404, 85 | body: '404 Not found', 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lambda/src/instagram.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export async function handler() { 4 | const endpoint = 'https://graph.instagram.com' 5 | const token = process.env.INSTAGRAM_ACCESS_TOKEN 6 | const date = new Date() 7 | 8 | const refreshToken = async function() { 9 | await axios.get(`${endpoint}/refresh_access_token`, { 10 | params: { 11 | access_token: token, 12 | grant_type: 'ig_refresh_token', 13 | }, 14 | }) 15 | } 16 | 17 | // Update the token every first day of the month 18 | if (date.getDate() === 1) { 19 | await refreshToken() 20 | } 21 | 22 | const { 23 | data: { data: posts }, 24 | } = await axios.get(`${endpoint}/me/media`, { 25 | params: { 26 | access_token: token, 27 | fields: 'id,media_url,permalink,caption,media_type', 28 | limit: 5, 29 | }, 30 | }) 31 | 32 | const getFirstAlbumMedia = async function(id) { 33 | const { 34 | data: { data: children }, 35 | } = await axios.get(`${endpoint}/${id}/children`, { 36 | params: { 37 | access_token: token, 38 | fields: 'id,media_url,permalink,media_type', 39 | }, 40 | }) 41 | 42 | if (children.length) { 43 | return children[0] 44 | } 45 | 46 | return {} 47 | } 48 | 49 | const resolveMediaType = function(item) { 50 | if (item.media_type === 'CAROUSEL_ALBUM') { 51 | const childMediaType = getFirstAlbumMedia(item.id).media_type 52 | 53 | return childMediaType === 'VIDEO' ? 'video' : 'image' 54 | } 55 | 56 | return item.media_type === 'VIDEO' ? 'video' : 'image' 57 | } 58 | 59 | return { 60 | statusCode: 200, 61 | headers: { 62 | 'content-type': 'application/json', 63 | }, 64 | body: JSON.stringify( 65 | posts.slice(0, 5).map(i => ({ 66 | id: i.id, 67 | link: i.permalink, 68 | media_url: i.media_url, 69 | media_type: resolveMediaType(i), 70 | caption: i.caption, 71 | })), 72 | ), 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lambda/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line import/no-extraneous-dependencies */ 2 | const Dotenv = require('dotenv-webpack') 3 | 4 | module.exports = { 5 | plugins: [new Dotenv()], 6 | } 7 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "./lambda/dist" 3 | 4 | [[headers]] 5 | for = "/*" 6 | [headers.values] 7 | Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strt/www", 3 | "description": "Strateg's website", 4 | "private": true, 5 | "version": "1.0.0", 6 | "author": "Alexander Nanberg ", 7 | "scripts": { 8 | "dev": "npm-run-all --parallel dev:*", 9 | "dev:client": "gatsby develop -H 0.0.0.0", 10 | "dev:lambda": "netlify-lambda serve lambda/src -c lambda/webpack.config.js", 11 | "build": "npm-run-all build:*", 12 | "build:client": "gatsby build", 13 | "build:lambda": "netlify-lambda build lambda/src", 14 | "start": "gatsby serve", 15 | "lint": "npm-run-all --continue-on-error --parallel lint:*", 16 | "lint:scripts": "eslint *.js", 17 | "lint:styles": "stylelint \"src/**/*.js\"", 18 | "format": "prettier --write \"**/*.{js,md,html,yaml,yml}\"" 19 | }, 20 | "husky": { 21 | "hooks": { 22 | "pre-commit": "lint-staged" 23 | } 24 | }, 25 | "lint-staged": { 26 | "*.js": [ 27 | "eslint", 28 | "stylelint", 29 | "prettier --write", 30 | "git add" 31 | ], 32 | "*.{md,html,yaml,yml}": [ 33 | "prettier --write", 34 | "git add" 35 | ] 36 | }, 37 | "dependencies": { 38 | "@contentful/rich-text-react-renderer": "^13.4.0", 39 | "@contentful/rich-text-types": "^13.4.0", 40 | "@fortawesome/fontawesome-svg-core": "^1.2.6", 41 | "@fortawesome/free-brands-svg-icons": "^5.4.1", 42 | "@fortawesome/pro-light-svg-icons": "^5.4.1", 43 | "@fortawesome/pro-regular-svg-icons": "^5.4.1", 44 | "@mdx-js/mdx": "^1.0.0", 45 | "@mdx-js/react": "^1.0.0", 46 | "@sendgrid/mail": "^6.3.1", 47 | "@sentry/browser": "^6.19.4", 48 | "@sentry/tracing": "^6.19.4", 49 | "axios": "^0.18.0", 50 | "change-case": "^3.1.0", 51 | "clipboard": "^2.0.8", 52 | "dayjs": "^1.7.7", 53 | "focus-trap": "^5.0.0", 54 | "focus-visible": "^4.1.5", 55 | "gatsby": "^2.18.0", 56 | "gatsby-image": "^2.2.16", 57 | "gatsby-mdx": "^0.6.0", 58 | "gatsby-plugin-catch-links": "^2.0.3", 59 | "gatsby-plugin-client-side-redirect": "^0.0.2", 60 | "gatsby-plugin-google-analytics": "^2.0.17", 61 | "gatsby-plugin-google-tagmanager": "^2.1.25", 62 | "gatsby-plugin-layout": "^1.0.12", 63 | "gatsby-plugin-manifest": "^2.0.26", 64 | "gatsby-plugin-meta-redirect": "^1.1.1", 65 | "gatsby-plugin-netlify": "^2.0.1", 66 | "gatsby-plugin-netlify-cache": "^1.0.0", 67 | "gatsby-plugin-react-helmet": "^3.0.0", 68 | "gatsby-plugin-robots-txt": "^1.5.0", 69 | "gatsby-plugin-sharp": "^2.3.3", 70 | "gatsby-plugin-sitemap": "^2.0.1", 71 | "gatsby-plugin-styled-components": "^3.0.0", 72 | "gatsby-plugin-subfont": "^1.0.1", 73 | "gatsby-remark-relative-images": "^0.2.0", 74 | "gatsby-source-contentful": "^2.1.30", 75 | "gatsby-source-filesystem": "^2.0.1", 76 | "gatsby-transformer-json": "^2.1.5", 77 | "gatsby-transformer-sharp": "^2.1.3", 78 | "js-cookie": "^2.2.0", 79 | "lodash.uniqueid": "^4.0.1", 80 | "matter-js": "^0.14.2", 81 | "nprogress": "^0.2.0", 82 | "polished": "^3.0.0", 83 | "prop-types": "^15.6.2", 84 | "query-string": "^6.3.0", 85 | "react": "^16.8.0", 86 | "react-dom": "^16.8.0", 87 | "react-helmet": "^5.2.0", 88 | "react-spring": "^8.0.0", 89 | "remark-external-links": "^4.0.0", 90 | "remark-unwrap-images": "^0.2.0", 91 | "resize-observer-polyfill": "^1.5.1", 92 | "styled-components": "^4.0.0", 93 | "styled-reset": "^2.0.0", 94 | "to-style": "^1.3.3", 95 | "unist-util-find-before": "^2.0.2", 96 | "unist-util-remove": "^1.0.1", 97 | "unist-util-visit-children": "^1.1.2" 98 | }, 99 | "devDependencies": { 100 | "@babel/core": "^7.7.4", 101 | "@babel/plugin-proposal-class-properties": "^7.4.0", 102 | "@babel/plugin-transform-object-assign": "^7.2.0", 103 | "@types/react": "^16.9.13", 104 | "babel-loader": "^8.0.5", 105 | "babel-plugin-styled-components": "^1.8.0", 106 | "dotenv-webpack": "^1.5.7", 107 | "eslint": "^5.6.1", 108 | "eslint-config-strateg": "^1.0.1", 109 | "http-proxy-middleware": "^0.19.0", 110 | "husky": "^2.0.0", 111 | "lint-staged": "^8.0.0", 112 | "netlify-lambda": "^1.4.4", 113 | "npm-run-all": "^4.1.5", 114 | "prettier": "^1.14.3", 115 | "stylelint": "^10.0.0", 116 | "stylelint-config-strateg": "^1.8.0", 117 | "typescript": "^3.7.2", 118 | "webpack": "^4.41.2", 119 | "webpack-bundle-analyzer": "^3.0.3" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /plugins/gatsby-plugin-nprogress/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | 3 | const defaultOptions = {} 4 | 5 | export const onClientEntry = (a, pluginOptions = {}) => { 6 | const options = { ...defaultOptions, ...pluginOptions } 7 | 8 | NProgress.configure(options) 9 | } 10 | 11 | export const onRouteUpdateDelayed = () => { 12 | NProgress.start() 13 | } 14 | 15 | export const onRouteUpdate = () => { 16 | NProgress.done() 17 | } 18 | -------------------------------------------------------------------------------- /plugins/gatsby-plugin-nprogress/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/plugins/gatsby-plugin-nprogress/index.js -------------------------------------------------------------------------------- /plugins/gatsby-plugin-nprogress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-plugin-nprogress", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /plugins/gatsby-remark-image-component/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const visitWithParents = require('unist-util-visit-parents') 3 | const path = require('path') 4 | const isRelativeUrl = require('is-relative-url') 5 | const { fluid } = require('gatsby-plugin-sharp') 6 | const slash = require('slash') 7 | 8 | async function generateImages({ 9 | node, 10 | getNode, 11 | markdownNode, 12 | files, 13 | resolve, 14 | reporter, 15 | options, 16 | }) { 17 | const parentNode = getNode(markdownNode.parent) 18 | let imagePath 19 | if (parentNode && parentNode.dir) { 20 | imagePath = slash(path.join(parentNode.dir, node.url)) 21 | } else { 22 | return null 23 | } 24 | 25 | const imageNode = files.find(file => file.absolutePath === imagePath) 26 | 27 | if (!imageNode || !imageNode.absolutePath) { 28 | return resolve() 29 | } 30 | 31 | const args = { 32 | maxWidth: 1780, 33 | quality: 80, 34 | srcSetBreakpoints: [365, 724, 960, 1440], 35 | ...options, 36 | } 37 | 38 | const fluidResult = await fluid({ 39 | file: imageNode, 40 | args, 41 | reporter, 42 | }) 43 | 44 | const webpResult = await fluid({ 45 | file: imageNode, 46 | args: { 47 | ...args, 48 | base64: false, 49 | toFormat: 'webp', 50 | }, 51 | reporter, 52 | }) 53 | 54 | return { 55 | ...fluidResult, 56 | srcSetWebp: webpResult.srcSet, 57 | } 58 | } 59 | 60 | module.exports = ( 61 | { files, markdownNode, markdownAST, getNode, reporter }, 62 | pluginOptions, 63 | ) => { 64 | const markdownImageNodes = [] 65 | visitWithParents(markdownAST, 'image', node => { 66 | markdownImageNodes.push({ node }) 67 | }) 68 | 69 | return Promise.all( 70 | markdownImageNodes.map( 71 | ({ node }) => 72 | new Promise(async resolve => { 73 | const fileType = node.url.slice(-3) 74 | 75 | if ( 76 | fileType !== 'gif' && 77 | fileType !== 'svg' && 78 | isRelativeUrl(node.url) 79 | ) { 80 | const image = await generateImages({ 81 | node, 82 | resolve, 83 | getNode, 84 | markdownNode, 85 | files, 86 | reporter, 87 | options: pluginOptions, 88 | }) 89 | 90 | if (image) { 91 | /* eslint-disable no-param-reassign */ 92 | node.type = 'element' 93 | node.data = { 94 | hName: 'image', 95 | hProperties: { 96 | ...image, 97 | }, 98 | } 99 | /* eslint-enable no-param-reassign */ 100 | } 101 | 102 | return resolve(node) 103 | } 104 | 105 | return resolve() 106 | }), 107 | ), 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /plugins/gatsby-remark-image-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-remark-image-component", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /plugins/rehype-wrap-in-columns.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const findBefore = require('unist-util-find-before') 3 | const visitChildren = require('unist-util-visit-children') 4 | 5 | function isTextElement(node) { 6 | return ( 7 | node.type === 'element' && 8 | (node.tagName === 'p' || 9 | node.tagName === 'a' || 10 | node.tagName === 'ul' || 11 | node.tagName === 'ol' || 12 | node.tagName === 'h2' || 13 | node.tagName === 'h3' || 14 | node.tagName === 'h4') 15 | ) 16 | } 17 | 18 | module.exports = () => tree => { 19 | let isNestedInColumn = false 20 | 21 | function visitor(node, index, parent) { 22 | // Toggle flag if node is a Column 23 | if (node.type === 'jsx' && node.value.includes('Column')) { 24 | isNestedInColumn = !node.value.startsWith('') 25 | return 'skip' 26 | } 27 | 28 | // Wrap or group node if it's not nested in a Column 29 | if (!isNestedInColumn && (node.type === 'element' || node.type === 'jsx')) { 30 | const prevSibling = findBefore(parent, node, 'element') 31 | const isNodeTextElement = isTextElement(node) 32 | 33 | // Move node to an existing column if the previous sibling is a text element 34 | if ( 35 | prevSibling && 36 | prevSibling.children && 37 | prevSibling.children.some(n => isTextElement(n)) && 38 | isNodeTextElement 39 | ) { 40 | prevSibling.children.push(node) 41 | parent.children.splice(index, 1) 42 | 43 | return index 44 | } 45 | 46 | // eslint-disable-next-line no-param-reassign 47 | parent.children[index] = { 48 | type: 'element', 49 | tagName: 'column', 50 | properties: { 51 | md: isNodeTextElement ? 8 : 12, 52 | bottomGap: isNodeTextElement ? 'large' : undefined, 53 | topGap: isNodeTextElement && index !== 0 ? 'small' : undefined, 54 | }, 55 | children: [node], 56 | } 57 | } 58 | 59 | return true 60 | } 61 | 62 | if (tree.children.length) { 63 | const visit = visitChildren(visitor) 64 | visit(tree) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [Strateg.se](https://strateg.se) 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/35441f05-a098-44a2-9f68-eec71d4a0d89/deploy-status)](https://app.netlify.com/sites/strateg/deploys) 4 | 5 | ## Requirements 6 | 7 | - Node 8+ (lts/carbon) 8 | - Yarn 9 | 10 | ## Setup 11 | 12 | - Run `cp .env.example .env` and fill in the required variables. 13 | 14 | - You will also need to have a global `FONTAWESOME_TOKEN` env variable set to access fontawsomes npm registry, run `export FONTAWESOME_TOKEN=`. 15 | 16 | - Run `yarn` to install dependencies and `yarn dev` to start the dev server. 17 | 18 | ## Deploy 19 | 20 | - The site is hosted on [Netlify](https://netlify.com). 21 | 22 | - We are using continous deployment so the `master` branch will be deployed on every commit. All pull requests are deployed as draft previews with a unique URL. 23 | 24 | ## Browser support 25 | 26 | - We only intend to support "evergreen" browsers (Chrome, Firefox, Safari and Edge). Full list is in our [.browserslistrc](.browserslistrc) file. 27 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { "enabled": false } 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fonttools 2 | brotli 3 | zopfli -------------------------------------------------------------------------------- /src/assets/fonts/Lineto EULA.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/Lineto EULA.pdf -------------------------------------------------------------------------------- /src/assets/fonts/circular-black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-black.woff -------------------------------------------------------------------------------- /src/assets/fonts/circular-black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-black.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/circular-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/circular-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-bold.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/circular-bolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-bolditalic.woff -------------------------------------------------------------------------------- /src/assets/fonts/circular-bolditalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-bolditalic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/circular-book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-book.woff -------------------------------------------------------------------------------- /src/assets/fonts/circular-book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-book.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/circular-bookitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-bookitalic.woff -------------------------------------------------------------------------------- /src/assets/fonts/circular-bookitalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-bookitalic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/circular-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-medium.woff -------------------------------------------------------------------------------- /src/assets/fonts/circular-medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/fonts/circular-medium.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/circular.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Legal Disclaimer 3 | * 4 | * These Fonts are licensed only for use on these domains and their subdomains: 5 | * strateg.se 6 | * 7 | * It is illegal to download or use them on other websites. 8 | * 9 | * While the @font-face statements below may be modified by the client, this 10 | * disclaimer may not be removed. 11 | * 12 | * Lineto.com, 2018 13 | */ 14 | 15 | @font-face { 16 | font-family: 'Circular'; 17 | font-display: fallback; 18 | src: url('./circular-black.woff2') format('woff2'), 19 | url('./circular-black.woff') format('woff'); 20 | font-weight: 900; 21 | font-style: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: 'Circular'; 26 | font-display: fallback; 27 | src: url('./circular-bold.woff2') format('woff2'), 28 | url('./circular-bold.woff') format('woff'); 29 | font-weight: 700; 30 | font-style: normal; 31 | } 32 | 33 | @font-face { 34 | font-family: 'Circular'; 35 | font-display: fallback; 36 | src: url('./circular-book.woff2') format('woff2'), 37 | url('./circular-book.woff') format('woff'); 38 | font-weight: normal; 39 | font-style: normal; 40 | } 41 | 42 | @font-face { 43 | font-family: 'Circular'; 44 | font-display: fallback; 45 | src: url('./circular-medium.woff2') format('woff2'), 46 | url('./circular-medium.woff') format('woff'); 47 | font-weight: 500; 48 | font-style: normal; 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strt/www/e96e52cfd78f31ba2c7f9fcc8f0830da936693fd/src/assets/icon.png -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MDXProvider } from '@mdx-js/react' 3 | import SkipToContentLink from './SkipToContentLink' 4 | import { IdProvider } from './IdManager' 5 | import NoSSR from './NoSSR' 6 | import CookieToast from './CookieToast' 7 | import components from './MDXComponents' 8 | import { GlobalStyle } from '../style' 9 | import '../lib/iconLibrary' 10 | import '../assets/fonts/circular.css' 11 | 12 | export default function App({ children }) { 13 | return ( 14 | 15 | 16 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Awards.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Image from './Image' 4 | import { breakpoints, vw } from '../style' 5 | 6 | export const AwardWrapper = styled.div` 7 | width: 20vw; 8 | height: auto; 9 | padding: ${vw(20)}; 10 | 11 | @media ${breakpoints.medium} { 12 | width: 15vw; 13 | padding: 15px; 14 | } 15 | 16 | @media ${breakpoints.large} { 17 | width: 10vw; 18 | } 19 | ` 20 | 21 | export const AwardsGrid = styled.div` 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | right: 0; 26 | bottom: 0; 27 | display: flex; 28 | flex-flow: row nowrap; 29 | align-items: flex-end; 30 | justify-content: flex-end; 31 | pointer-events: none; 32 | ` 33 | 34 | export default function Awards({ ...props }) { 35 | return ( 36 | 37 | {props.items.map(item => ( 38 | 39 | {(() => { 40 | return ( 41 | {item.caption} 46 | ) 47 | })()} 48 | 49 | ))} 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Box.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { H2, Text } from './Text' 4 | import Link from './Link' 5 | import { fluidRange, colors, breakpoints, vw } from '../style' 6 | 7 | export const BoxWrapper = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | flex-grow: 1; 12 | padding: ${fluidRange({ min: 32, max: 56 })}; 13 | background-color: ${props => props.bg || colors.ice}; 14 | grid-column: col-start 2 / col-end 12; 15 | grid-row: 2/5; 16 | 17 | @media ${breakpoints.medium} { 18 | padding: ${vw(108)} ${vw(120)}; 19 | grid-column: col-start 6 / col-end 11; 20 | grid-row: 5/7; 21 | } 22 | 23 | *:last-child { 24 | margin-bottom: 0; 25 | } 26 | ` 27 | 28 | export default function Box({ title, content, link, ...props }) { 29 | return ( 30 | 31 |

{title}

32 | {content} 33 | {link && ( 34 | 35 | {link.text} 36 | 37 | )} 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/BoxSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Image from './Image' 4 | import { CssGrid } from './Grid' 5 | import { breakpoints, colors } from '../style' 6 | import Box from './Box' 7 | 8 | const Background = styled.div` 9 | grid-column: full-start/col-end 12; 10 | grid-row: 1/3; 11 | background-color: ${colors.steel500}; 12 | 13 | img { 14 | width: 100%; 15 | object-fit: cover; 16 | } 17 | 18 | @media ${breakpoints.medium} { 19 | grid-column: full-start/col-end 11; 20 | grid-row: 1/6; 21 | } 22 | ` 23 | 24 | const StyledBox = styled(Box)` 25 | position: relative; 26 | z-index: 2; 27 | grid-column: col-start 2 / col-end 12; 28 | grid-row: 2/5; 29 | 30 | @media ${breakpoints.medium} { 31 | grid-column: col-start 6 / col-end 11; 32 | grid-row: 5/7; 33 | } 34 | ` 35 | 36 | export default function BoxSection({ 37 | backgroundImage, 38 | title, 39 | excerpt, 40 | link, 41 | boxBg, 42 | }) { 43 | return ( 44 | 45 | 46 | {backgroundImage && } 47 | 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Icon from './Icon' 4 | import { colors, fluidRange, breakpoints, vw, fontFamily } from '../style' 5 | 6 | export const BtnReset = styled.button` 7 | display: inline-block; 8 | border: none; 9 | text-align: center; 10 | text-decoration: none; 11 | line-height: normal; 12 | 13 | &:focus { 14 | outline: none; 15 | box-shadow: none; 16 | } 17 | ` 18 | 19 | const StyledButton = styled.button` 20 | display: inline-block; 21 | vertical-align: middle; 22 | margin: 0; 23 | border: none; 24 | border-radius: 0; 25 | padding: ${fluidRange({ min: 18, max: 24 })}; 26 | font-size: ${fluidRange({ min: 18, max: 24 })}; 27 | font-weight: 400; 28 | color: white; 29 | font-family: ${fontFamily}; 30 | background-color: ${colors.dark}; 31 | box-shadow: none; 32 | appearance: none; 33 | 34 | @media ${breakpoints.medium} { 35 | padding: ${vw(24)}; 36 | font-size: ${vw(24)}; 37 | } 38 | ` 39 | 40 | export const ButtonInner = styled.div` 41 | display: flex; 42 | justify-content: space-between; 43 | align-items: center; 44 | ` 45 | 46 | export default function Button(props) { 47 | return 48 | } 49 | 50 | export const IconButton = styled.button` 51 | appearance: none; 52 | padding: 0; 53 | margin: 0; 54 | border: none; 55 | font-size: ${fluidRange({ min: 20, max: 26 })}; 56 | line-height: 0.625em; 57 | color: ${props => props.textColor || colors.dark}; 58 | background: none; 59 | 60 | @media ${breakpoints.medium} { 61 | font-size: ${vw(32)}; 62 | } 63 | 64 | &:focus { 65 | outline: none; 66 | } 67 | ` 68 | 69 | export const IconArrow = styled.button` 70 | appearance: none; 71 | padding: 0; 72 | margin: 0; 73 | border: none; 74 | font-size: 3rem; 75 | line-height: 0.625em; 76 | color: ${props => props.textColor || colors.dark}; 77 | background: none; 78 | cursor: pointer; 79 | 80 | &:focus { 81 | outline: none; 82 | } 83 | ` 84 | 85 | export function ScrollToTopButton(props) { 86 | return ( 87 | { 90 | event.preventDefault() 91 | window.scroll({ 92 | top: 0, 93 | behavior: 'smooth', 94 | }) 95 | }} 96 | {...props} 97 | > 98 | 99 | 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/components/Card.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { Link as GatsbyLink, graphql } from 'gatsby' 3 | import styled from 'styled-components' 4 | import dayjs from 'dayjs' 5 | import { Text } from './Text' 6 | import Image from './Image' 7 | import { colors, ratio, breakpoints, cover, fluidRange, vw } from '../style' 8 | import { getWidth } from './Grid' 9 | 10 | const Link = styled(GatsbyLink)` 11 | display: block; 12 | outline: none; 13 | text-decoration: none; 14 | -webkit-tap-highlight-color: transparent; 15 | ` 16 | 17 | const Article = styled.article` 18 | display: flex; 19 | ` 20 | 21 | const Content = styled.div` 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: space-between; 25 | padding: ${fluidRange({ min: 16, max: 24 })}; 26 | width: ${getWidth(8)}; 27 | background-color: ${colors.light}; 28 | 29 | p { 30 | margin-bottom: 0; 31 | 32 | /* Copy/mimic Link component style */ 33 | & span { 34 | ${Link}:hover & { 35 | text-decoration: underline; 36 | } 37 | 38 | ${Link}.focus-visible & { 39 | text-decoration: none; 40 | background-color: ${colors.orange300}; 41 | } 42 | 43 | ${Link}:active & { 44 | text-decoration: none; 45 | background-color: ${colors.orange100}; 46 | } 47 | } 48 | } 49 | 50 | @media ${breakpoints.medium} { 51 | padding: ${vw(24)}; 52 | width: ${getWidth(6)}; 53 | } 54 | ` 55 | 56 | const DateTime = styled(Text)` 57 | margin-bottom: 0.5rem; 58 | font-size: 1rem; 59 | line-height: 1.4; 60 | 61 | @media ${breakpoints.medium} { 62 | font-size: 1.125rem; 63 | } 64 | ` 65 | 66 | const ImageWrapper = styled.div` 67 | ${ratio({ x: 4, y: 3 })} 68 | width: ${getWidth(4)}; 69 | overflow: hidden; 70 | 71 | @media ${breakpoints.medium} { 72 | width: ${getWidth(6)}; 73 | } 74 | 75 | * { 76 | ${cover()} 77 | } 78 | ` 79 | 80 | export default function Card({ url, title, date, image }) { 81 | const formattedDate = useMemo( 82 | () => (date ? dayjs(date).format('D MMM YYYY') : null), 83 | [date], 84 | ) 85 | 86 | return ( 87 | 88 |
89 | 90 | {date && ( 91 | 97 | {formattedDate} 98 | 99 | )} 100 | 101 | {title} 102 | 103 | 104 | 105 | {image && ( 106 | 112 | )} 113 | 114 |
115 | 116 | ) 117 | } 118 | 119 | export const query = graphql` 120 | fragment CardImage on ImageSharp { 121 | fluid(quality: 80, srcSetBreakpoints: [200, 340, 520]) { 122 | ...GatsbyImageSharpFluid_withWebp 123 | } 124 | } 125 | ` 126 | -------------------------------------------------------------------------------- /src/components/CardInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Link from './Link' 4 | import { TextSmall, H3 } from './Text' 5 | import { colors, breakpoints } from '../style' 6 | 7 | const CardWrapper = styled.div` 8 | bottom: -40%; 9 | right: 0; 10 | margin-left: auto; 11 | max-width: 75%; 12 | padding: 2rem; 13 | background-color: ${colors.orange100}; 14 | 15 | h3 { 16 | padding-bottom: 1rem; 17 | } 18 | 19 | p:last-child { 20 | margin-bottom: 0; 21 | } 22 | 23 | @media ${breakpoints.small} { 24 | bottom: -20%; 25 | right: 0; 26 | max-width: 50%; 27 | padding: 4rem 4rem; 28 | } 29 | 30 | @media ${breakpoints.medium} { 31 | bottom: -30%; 32 | right: 0; 33 | max-width: 50%; 34 | padding: 4rem 5.5rem; 35 | } 36 | 37 | @media ${breakpoints.large} { 38 | bottom: -20%; 39 | padding: 5rem 6rem; 40 | } 41 | ` 42 | 43 | export default function Card({ title, text, link, linkText, position }) { 44 | return ( 45 | 46 |

{title}

47 | {text} 48 | 49 | {linkText} 50 | 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/CleanTag.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const defaultBlacklist = [ 4 | 'mt', 5 | 'mb', 6 | 'my', 7 | 'pt', 8 | 'pb', 9 | 'py', 10 | 'width', 11 | 'sm', 12 | 'smDown', 13 | 'md', 14 | 'lg', 15 | 'justifyContent', 16 | 'alignItems', 17 | 'flexWrap', 18 | 'textAlign', 19 | 'bottomGap', 20 | 'topGap', 21 | 'variant', 22 | 'colorVariant', 23 | ] 24 | 25 | export function omitInvalidProps(props, keys = defaultBlacklist) { 26 | return Object.entries(props).reduce((acc, [key, value]) => { 27 | if (!keys.includes(key)) { 28 | acc[key] = value 29 | } 30 | 31 | return acc 32 | }, {}) 33 | } 34 | 35 | export const CleanTag = React.forwardRef( 36 | ({ as: Tag = 'div', blacklist = defaultBlacklist, ...props }, ref) => 37 | React.createElement(Tag, { 38 | ref, 39 | ...omitInvalidProps(props, blacklist), 40 | }), 41 | ) 42 | -------------------------------------------------------------------------------- /src/components/ContentWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { ThemeProvider } from 'styled-components' 3 | import { Column, getColumnMargin } from './Grid' 4 | import { Text } from './Text' 5 | import { ImageWrapper } from './Image' 6 | import { OrderedList, UnorderedList } from './List' 7 | 8 | const ContentWrapperStyle = styled.div` 9 | ${Column} { 10 | display: flex; 11 | flex-direction: column; 12 | 13 | ${ImageWrapper}:not(:last-child) { 14 | ${props => getColumnMargin(props)} 15 | } 16 | } 17 | 18 | ${Text} { 19 | &:last-child { 20 | margin-bottom: 0; 21 | } 22 | } 23 | 24 | ${UnorderedList} { 25 | &:last-child { 26 | margin-bottom: 1em; 27 | } 28 | } 29 | 30 | ${OrderedList} { 31 | &:last-child { 32 | margin-bottom: 1em; 33 | } 34 | } 35 | ` 36 | 37 | export default function ContentWrapper({ children }) { 38 | return ( 39 | 40 | {children} 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Cover.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import styled from 'styled-components' 4 | import { colors, breakpoints } from '../style' 5 | 6 | const CoverWrapper = styled.div` 7 | position: relative; 8 | height: 0; 9 | width: 100%; 10 | padding-top: ${props => (props.isVideo ? '56.25%' : '100%')}; 11 | overflow: hidden; 12 | background-color: ${props => props.bg || colors.dark}; 13 | 14 | @media screen and ${breakpoints.medium} { 15 | padding-top: 56.25%; 16 | } 17 | 18 | > *, 19 | video, 20 | img { 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | object-fit: cover; 27 | } 28 | ` 29 | 30 | export default function Cover(props) { 31 | return 32 | } 33 | 34 | export const query = graphql` 35 | fragment CoverImage on ImageSharp { 36 | fluid(maxWidth: 1520, quality: 80) { 37 | ...GatsbyImageSharpFluid_withWebp 38 | } 39 | } 40 | ` 41 | -------------------------------------------------------------------------------- /src/components/Dialog.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useContext } from 'react' 2 | import styled from 'styled-components' 3 | import { useTransition, animated, config } from 'react-spring' 4 | import Portal from './Portal' 5 | import Div from './Div' 6 | import Icon from './Icon' 7 | import Button, { ButtonInner, IconButton } from './Button' 8 | import useFocusTrap from '../lib/useFocusTrap' 9 | import useDisableScroll from '../lib/useDisableScroll' 10 | import { vw, breakpoints, colors, fluidRange } from '../style' 11 | 12 | const FocusContext = React.createContext() 13 | 14 | export function DialogOverlay({ 15 | onDismiss, 16 | isOpen, 17 | initialFocusRef, 18 | ...props 19 | }) { 20 | const overlayRef = useRef(null) 21 | const contentRef = useRef(null) 22 | useFocusTrap(overlayRef, { 23 | initialFocusRef, 24 | fallbackFocusRef: contentRef, 25 | shouldTrap: isOpen, 26 | }) 27 | useDisableScroll(isOpen) 28 | 29 | return ( 30 | 31 | { 34 | event.stopPropagation() 35 | onDismiss() 36 | }} 37 | onKeyDown={event => { 38 | if (event.key === 'Escape') { 39 | event.stopPropagation() 40 | onDismiss() 41 | } 42 | }} 43 | {...props} 44 | /> 45 | 46 | ) 47 | } 48 | 49 | export function DialogContent({ onClick, onKeyDown, ...props }) { 50 | const contentRef = useContext(FocusContext) 51 | 52 | return ( 53 | { 59 | event.stopPropagation() 60 | }} 61 | {...props} 62 | /> 63 | ) 64 | } 65 | 66 | export default function Dialog({ isOpen, onDismiss, children, ...props }) { 67 | const transitions = useTransition(isOpen, null, { 68 | unique: true, 69 | from: { opacity: 0, transform: `translate3d(0, 24px, 0) scale(0.98)` }, 70 | enter: { 71 | opacity: 1, 72 | transform: `translate3d(0, 0, 0) scale(1)`, 73 | pointerEvents: 'auto', 74 | }, 75 | leave: { 76 | opacity: 0, 77 | transform: `translate3d(0, 8px, 0) scale(0.98)`, 78 | pointerEvents: 'none', 79 | }, 80 | config: { ...config.stiff, tension: 300 }, 81 | }) 82 | 83 | return transitions.map( 84 | ({ item: show, props: { transform, ...style }, key }) => 85 | show && ( 86 | 87 | 88 | 89 | 90 | {children} 91 | 92 | 93 | 94 | ), 95 | ) 96 | } 97 | 98 | export function DialogButton(props) { 99 | return ( 100 | 103 | ) 104 | } 105 | 106 | export function DialogCloseButton(props) { 107 | return ( 108 | 113 | 114 | 115 | 116 | 117 | ) 118 | } 119 | 120 | const StyledDialogOverlay = animated(styled.div` 121 | position: fixed; 122 | z-index: 11; 123 | top: 0; 124 | right: 0; 125 | bottom: 0; 126 | left: 0; 127 | display: flex; 128 | flex-direction: column; 129 | align-items: center; 130 | padding: ${fluidRange({ min: 24, max: 32 })} 0; 131 | overflow-y: auto; 132 | overscroll-behavior: contain; 133 | -webkit-overflow-scrolling: touch; 134 | background-color: rgba(0, 0, 0, 0.8); 135 | 136 | @media ${breakpoints.medium} { 137 | padding: ${vw(32)} 0; 138 | } 139 | `) 140 | 141 | const StyledDialogContent = animated(styled.div` 142 | position: relative; 143 | flex-shrink: 0; 144 | width: 94%; 145 | max-width: 480px; 146 | margin: auto; 147 | outline: none; 148 | background-color: white; 149 | box-shadow: 0 30px 40px 10px #0b101e33; 150 | 151 | @media ${breakpoints.large} { 152 | width: 100%; 153 | max-width: ${vw(624)}; 154 | } 155 | `) 156 | 157 | export const DialogRow = styled(Div)` 158 | padding-right: ${fluidRange({ min: 16, max: 24 })}; 159 | padding-left: ${fluidRange({ min: 16, max: 24 })}; 160 | 161 | @media ${breakpoints.medium} { 162 | padding-right: ${vw(56)}; 163 | padding-left: ${vw(56)}; 164 | } 165 | ` 166 | 167 | export const DialogActions = styled(Div).attrs({ mt: [4, 8] })` 168 | display: flex; 169 | 170 | & > * { 171 | flex-grow: 1; 172 | } 173 | ` 174 | -------------------------------------------------------------------------------- /src/components/Div.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { space, bgColor } from '../style' 3 | 4 | const Div = styled.div( 5 | props => { 6 | if (props.halfTopBg) { 7 | return { 8 | position: 'relative', 9 | '&::before': { 10 | content: "''", 11 | position: 'absolute', 12 | zIndex: 0, 13 | top: 0, 14 | left: 0, 15 | right: 0, 16 | height: '50%', 17 | backgroundColor: props.halfTopBg, 18 | }, 19 | '> *': { 20 | position: 'relative', 21 | }, 22 | } 23 | } 24 | 25 | return null 26 | }, 27 | space, 28 | bgColor, 29 | ) 30 | 31 | export default Div 32 | -------------------------------------------------------------------------------- /src/components/EmbedPlayer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { ratio, cover } from '../style' 4 | 5 | const PlayerWrapper = styled.div` 6 | background-color: ${props => props.bg || 'transparent'}; 7 | ${props => 8 | ratio( 9 | props.aspectRatio 10 | ? { x: props.aspectRatio[0], y: props.aspectRatio[1] } 11 | : undefined, 12 | )} 13 | 14 | iframe { 15 | ${cover()} 16 | object-position: left; 17 | } 18 | ` 19 | 20 | export default function EmbedPlayer({ title, bg, aspectRatio, ...props }) { 21 | return ( 22 | 23 |