├── src ├── components │ ├── index.js │ ├── common │ │ ├── index.js │ │ ├── HtmlContent.js │ │ └── Layout.js │ └── legal │ │ ├── MobileNavigationIcon.js │ │ ├── index.js │ │ ├── LegalPageSection.js │ │ ├── MobileNavigationButton.js │ │ ├── NavigationItem.js │ │ ├── LegalPageBody.js │ │ ├── LegalPageNavigation.js │ │ └── LegalPageHero.js ├── gatsby-plugin-theme-ui │ └── index.js ├── util │ └── helpers.js ├── styles │ ├── htmlContentStyle.js │ ├── theme.js │ └── global.js ├── schemas │ └── legal.json └── templates │ └── legal.js ├── index.js ├── gatsby-config.js ├── .gitignore ├── gatsby-node.js ├── package.json └── README.md /src/components/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | // Left blank on purpose 3 | -------------------------------------------------------------------------------- /src/gatsby-plugin-theme-ui/index.js: -------------------------------------------------------------------------------- 1 | import { theme } from "../styles/theme" 2 | export default theme 3 | -------------------------------------------------------------------------------- /src/components/common/index.js: -------------------------------------------------------------------------------- 1 | import HtmlContent from './HtmlContent'; 2 | import Layout from './Layout'; 3 | 4 | export { 5 | HtmlContent, 6 | Layout, 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/common/HtmlContent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HtmlContentStyle from '../../styles/htmlContentStyle'; 3 | 4 | const HtmlContent = ({ content }) => ( 5 | <> 6 |
10 | 11 | 12 | ); 13 | 14 | export default HtmlContent; 15 | -------------------------------------------------------------------------------- /src/util/helpers.js: -------------------------------------------------------------------------------- 1 | export const getSectionId = (index, title) => `${getSectionAffix(index)}-${toKebabCase(title)}`; 2 | export const getSectionAffix = index => `${(index < 9) ? `0${index + 1}` : index + 1}`; 3 | export const isClient = typeof window !== 'undefined'; 4 | export const toKebabCase = str => { 5 | return str 6 | .toLowerCase() 7 | .replace(/[^a-z0-9]+/g, "-") 8 | .replace(/(^-|-$)+/g, "") 9 | } 10 | -------------------------------------------------------------------------------- /src/components/legal/MobileNavigationIcon.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui'; 3 | import theme from '../../styles/theme'; 4 | 5 | const MobileNavigationIcon = () => ( 6 | 15 | 16 | 17 | ); 18 | 19 | export default MobileNavigationIcon; 20 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ 2 | prismicRepositoryName, 3 | prismicAccessToken, 4 | siteName = null, 5 | homePath = '/', 6 | }) => ({ 7 | siteMetadata: { 8 | homePath, 9 | siteName, 10 | }, 11 | plugins: [ 12 | 'gatsby-plugin-theme-ui', 13 | { 14 | resolve: 'gatsby-source-prismic', 15 | options: { 16 | repositoryName: prismicRepositoryName, 17 | accessToken: prismicAccessToken, 18 | schemas: { 19 | legal: require('./src/schemas/legal.json'), 20 | }, 21 | }, 22 | }, 23 | ], 24 | }) 25 | -------------------------------------------------------------------------------- /src/components/legal/index.js: -------------------------------------------------------------------------------- 1 | import LegalPageBody from './LegalPageBody'; 2 | import LegalPageHero from './LegalPageHero'; 3 | import LegalPageSection from './LegalPageSection'; 4 | import LegalPageNavigation from './LegalPageNavigation'; 5 | import MobileNavigationButton from './MobileNavigationButton'; 6 | import MobileNavigationIcon from './MobileNavigationIcon'; 7 | import NavigationItem from './NavigationItem'; 8 | 9 | export { 10 | LegalPageBody, 11 | LegalPageHero, 12 | LegalPageSection, 13 | LegalPageNavigation, 14 | MobileNavigationButton, 15 | MobileNavigationIcon, 16 | NavigationItem, 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/common/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import { Layout as ThemeLayout, Main } from 'theme-ui'; 4 | import GlobalStyle from '../../styles/global'; 5 | 6 | const Layout = (props) => { 7 | const { 8 | children, 9 | // location, 10 | seoData, 11 | } = props; 12 | const { 13 | metaTitle = null, 14 | metaDescription = null, 15 | openGraphImage = null, 16 | } = seoData; 17 | return ( 18 | 19 | 20 | {metaTitle && metaTitle.text && ( 21 | 22 | { metaDescription && metaDescription.text && ( 23 | 24 | )} 25 | { openGraphImage && openGraphImage.url && ( 26 | 27 | )} 28 | 29 | )} 30 |
31 | {children} 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Layout; 38 | -------------------------------------------------------------------------------- /src/components/legal/LegalPageSection.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui' 3 | import { HtmlContent } from '../common' 4 | import { getSectionId } from '../../util/helpers' 5 | import { InView } from 'react-intersection-observer' 6 | import { Styled } from 'theme-ui' 7 | 8 | const LegalPageSection = ({ index, section, sectionInViewHandler }) => { 9 | const { 10 | sectionHeading, 11 | content, 12 | } = section 13 | const sectionId = getSectionId(index, sectionHeading.text) 14 | return ( 15 | sectionInViewHandler(index, inView)} 25 | > 26 | 32 | {sectionHeading.text} 33 | 34 | 37 | 38 | ) 39 | }; 40 | 41 | export default LegalPageSection; 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | .idea 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | 51 | 52 | # Build Files 53 | public/ 54 | .cache/ 55 | 56 | # Gatsby context 57 | .gatsby-context.js 58 | 59 | # Bundle stats 60 | bundle-stats.json 61 | 62 | netlify.toml 63 | .env.development 64 | .env.production 65 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | // graphql function doesn't throw an error so we have to check to check for the result.errors to throw manually 2 | const wrapper = promise => promise.then((result) => { 3 | if (result.errors) throw result.errors; 4 | return result; 5 | }); 6 | 7 | 8 | exports.createPages = async ({ graphql, actions }) => { 9 | const { createPage } = actions; 10 | 11 | const legalPageTemplate = require.resolve('./src/templates/legal.js'); 12 | 13 | const legalPages = await wrapper( 14 | graphql(` 15 | { 16 | allPrismicLegal { 17 | edges { 18 | node { 19 | id 20 | uid 21 | } 22 | } 23 | } 24 | } 25 | `), 26 | ); 27 | 28 | const legalPageList = legalPages.data.allPrismicLegal.edges; 29 | 30 | /* --------------------------------------------- 31 | = Create an individual page for each Information page = 32 | ----------------------------------------------- */ 33 | 34 | legalPageList.forEach((edge) => { 35 | // The uid you assigned in Prismic is the slug! 36 | createPage({ 37 | path: `/${edge.node.uid}/`, 38 | component: legalPageTemplate, 39 | context: { 40 | // Pass the unique ID (uid) through context so the template can filter by it 41 | uid: edge.node.uid, 42 | }, 43 | }); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@littleplusbig/gatsby-theme-legals-prismic", 3 | "author": "Allan Pooley", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/littleplusbig/gatsby-theme-legals-prismic" 7 | }, 8 | "bugs": { 9 | "url": "https://github.com/littleplusbig/gatsby-theme-legals-prismic/issues" 10 | }, 11 | "keywords": [ 12 | "gatsby", 13 | "gatsby-theme", 14 | "gatsby-plugin", 15 | "prismic", 16 | "terms and conditions", 17 | "privacy policy", 18 | "legal" 19 | ], 20 | "version": "1.0.18", 21 | "license": "MIT", 22 | "main": "index.js", 23 | "scripts": { 24 | "build": "gatsby build", 25 | "develop": "gatsby develop", 26 | "clean": "gatsby clean" 27 | }, 28 | "peerDependencies": { 29 | "gatsby": "^2.13.41", 30 | "react": "^16.8.6", 31 | "react-dom": "^16.8.6" 32 | }, 33 | "devDependencies": { 34 | "gatsby": "^2.13.41", 35 | "react": "^16.8.6", 36 | "react-dom": "^16.8.6" 37 | }, 38 | "dependencies": { 39 | "@emotion/core": "^10.0.14", 40 | "@emotion/styled": "^10.0.14", 41 | "@mdx-js/react": "^1.0.27", 42 | "gatsby-plugin-theme-ui": "^0.2.25", 43 | "gatsby-source-prismic": "^2.3.0-alpha.3", 44 | "react-helmet": "^5.2.1", 45 | "react-intersection-observer": "^8.24.1", 46 | "styled-components": "^4.3.2", 47 | "theme-ui": "^0.2.25" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/legal/MobileNavigationButton.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui'; 3 | import { 4 | MobileNavigationIcon 5 | } from '.'; 6 | import { 7 | getSectionAffix, 8 | } from '../../util/helpers'; 9 | 10 | const MobileNavigationButton = ({ 11 | navOpen, 12 | activeSection, 13 | sectionTitles, 14 | setNavOpenHandler 15 | }) => ( 16 | 50 | ); 51 | 52 | export default MobileNavigationButton; 53 | -------------------------------------------------------------------------------- /src/components/legal/NavigationItem.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui' 3 | import { 4 | getSectionAffix, 5 | getSectionId, 6 | } from '../../util/helpers' 7 | 8 | const NavigationItem = ({ 9 | index, 10 | isActive, 11 | scrollToHandler, 12 | sectionTitle, 13 | }) => ( 14 |
  • 20 | 55 |
  • 56 | ); 57 | 58 | export default NavigationItem; 59 | -------------------------------------------------------------------------------- /src/styles/htmlContentStyle.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import theme from './theme'; 3 | 4 | const HtmlContentStyle = createGlobalStyle` 5 | .gatsby-theme-legals-html-content { 6 | * { 7 | color: ${theme.colors.text} 8 | } 9 | 10 | em { 11 | font-style: italic; 12 | } 13 | 14 | p { 15 | margin-bottom: 20px; 16 | 17 | strong { 18 | font-weight: ${theme.fontWeights.bold}; 19 | } 20 | } 21 | 22 | a { 23 | text-decoration: underline; 24 | } 25 | 26 | h2, 27 | h3, 28 | h4, 29 | h5, 30 | h6 { 31 | margin-top: 30px; 32 | margin-bottom: 20px; 33 | 34 | &:first-child { 35 | margin-top: 0; 36 | } 37 | } 38 | 39 | h2 { 40 | margin-top: 60px; 41 | 42 | &:first-child { 43 | margin-top: 0; 44 | } 45 | } 46 | 47 | h3 { 48 | margin-top: 40px; 49 | 50 | &:first-child { 51 | margin-top: 0; 52 | } 53 | } 54 | 55 | a { 56 | text-decoration: underline; 57 | 58 | &:hover { 59 | text-decoration: none; 60 | } 61 | } 62 | 63 | ul, ol { 64 | width: 90%; 65 | padding-left: 25px; 66 | margin-bottom: 30px; 67 | 68 | li { 69 | margin-left: 15px; 70 | margin-bottom: 20px; 71 | } 72 | } 73 | 74 | ul { 75 | list-style-type: disc; 76 | 77 | li { 78 | list-style-type: disc; 79 | position: relative; 80 | } 81 | } 82 | 83 | ol { 84 | list-style-type: decimal; 85 | 86 | li { 87 | list-style-type: decimal; 88 | margin-left: 10px; 89 | text-indent: 0.5em; 90 | } 91 | } 92 | } 93 | `; 94 | export default HtmlContentStyle; 95 | -------------------------------------------------------------------------------- /src/components/legal/LegalPageBody.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui'; 3 | import { LegalPageNavigation, LegalPageSection } from '.'; 4 | 5 | const LegalPageBody = ({ activeSection, sectionInViewHandler, sections }) => ( 6 |
    12 |
    23 |
    33 | 37 |
    38 |
    46 | { sections && sections.map((section, index) => ( 47 | 53 | ))} 54 |
    55 |
    56 |
    57 | ) 58 | 59 | export default LegalPageBody 60 | -------------------------------------------------------------------------------- /src/schemas/legal.json: -------------------------------------------------------------------------------- 1 | { 2 | "Main": { 3 | "page_name": { 4 | "type": "StructuredText", 5 | "config": { 6 | "single": "heading1", 7 | "label": "Page Name", 8 | "placeholder": "Privacy Policy" 9 | } 10 | }, 11 | "uid": { 12 | "type": "UID", 13 | "config": { 14 | "label": "Slug", 15 | "placeholder": "privacy-policy" 16 | } 17 | }, 18 | "hero_subtitle": { 19 | "type": "StructuredText", 20 | "config": { 21 | "single": "paragraph", 22 | "label": "Hero Subtitle", 23 | "placeholder": "How we manage your data" 24 | } 25 | }, 26 | "sections": { 27 | "type": "Group", 28 | "config": { 29 | "fields": { 30 | "section_heading": { 31 | "type": "StructuredText", 32 | "config": { 33 | "single": "heading2", 34 | "label": "Section Heading", 35 | "placeholder": "General information" 36 | } 37 | }, 38 | "content": { 39 | "type": "StructuredText", 40 | "config": { 41 | "multi": "paragraph, preformatted, heading3, strong, em, hyperlink, list-item, o-list-item, o-list-item", 42 | "allowTargetBlank": true, 43 | "label": "Content", 44 | "placeholder": "Information on this website is of a general nature. Our company has ..." 45 | } 46 | } 47 | }, 48 | "label": "Sections" 49 | } 50 | } 51 | }, 52 | "SEO": { 53 | "meta_title": { 54 | "type": "StructuredText", 55 | "config": { 56 | "single": "heading1", 57 | "label": "Meta Title", 58 | "placeholder": "Enter meta title" 59 | } 60 | }, 61 | "meta_description": { 62 | "type": "StructuredText", 63 | "config": { 64 | "single": "paragraph", 65 | "label": "Meta Description", 66 | "placeholder": "Enter meta description" 67 | } 68 | }, 69 | "open_graph_image": { 70 | "type": "Image", 71 | "config": { 72 | "constraint": { 73 | "width": 1200, 74 | "height": 630 75 | }, 76 | "thumbnails": [], 77 | "label": "Open Graph Image" 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/styles/theme.js: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | space: [0, 4, 8, 16, 32], 3 | breakpoints: [ 4 | '500px', '800px', '1080px', 5 | ], 6 | fonts: { 7 | body: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif', 8 | heading: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif', 9 | }, 10 | fontSizes: [14, 16, 20, 24, 36, 44, 64, 80], 11 | fontWeights: { 12 | body: 400, 13 | regular: 400, 14 | medium: 500, 15 | subheading: 500, 16 | heading: 700, 17 | bold: 700, 18 | }, 19 | lineHeights: { 20 | body: 1.5, 21 | heading: 1.2, 22 | }, 23 | letterSpacings: { 24 | body: 'normal', 25 | caps: '0.2em', 26 | }, 27 | colors: { 28 | text: '#333333', 29 | background: '#FFFFFF', 30 | primary: '#6F2B9F', 31 | primaryDark: '#5B2589', 32 | primaryLight: '#BB75D1', 33 | white: '#FFFFFF', 34 | offWhite: '#FCFAFF', 35 | black: '#000000', 36 | offBlack: '#333333', 37 | grey: '#F3F3F3', 38 | }, 39 | sizes: { 40 | wrapper: '1240px', 41 | }, 42 | textStyles: { 43 | heading: { 44 | fontFamily: 'heading', 45 | lineHeight: 'heading', 46 | fontWeight: 'heading', 47 | color: 'text', 48 | }, 49 | label: { 50 | fontSize: [2, 2, 3, 3], 51 | fontFamily: 'heading', 52 | lineHeight: 'heading', 53 | fontWeight: 'heading', 54 | color: 'text', 55 | }, 56 | controls: { 57 | fontSize: [1, 1, 1, 1], 58 | fontFamily: 'heading', 59 | fontWeight: 'regular', 60 | lineHeight: 'heading', 61 | textTransform: 'uppercase', 62 | letterSpacing: '0.05em', 63 | }, 64 | }, 65 | styles: { 66 | root: { 67 | fontFamily: 'body', 68 | fontWeight: 'body', 69 | lineHeight: 'body', 70 | }, 71 | h1: { 72 | variant: 'textStyles.heading', 73 | fontSize: [5, 6, 6, 7], 74 | }, 75 | h2: { 76 | variant: 'textStyles.heading', 77 | fontSize: [3, 3, 4, 4], 78 | }, 79 | h3: { 80 | variant: 'textStyles.heading', 81 | fontSize: 3, 82 | }, 83 | h4: { 84 | variant: 'textStyles.heading', 85 | fontSize: 2, 86 | }, 87 | h5: { 88 | variant: 'textStyles.heading', 89 | fontSize: 1, 90 | }, 91 | h6: { 92 | variant: 'textStyles.heading', 93 | fontSize: 0, 94 | }, 95 | span: { 96 | variant: 'textStyles.label', 97 | }, 98 | }, 99 | } 100 | 101 | export default theme 102 | -------------------------------------------------------------------------------- /src/components/legal/LegalPageNavigation.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui' 3 | import { useState, useEffect } from 'react' 4 | import { 5 | isClient, 6 | } from '../../util/helpers' 7 | import { 8 | MobileNavigationButton, 9 | NavigationItem, 10 | } from '.'; 11 | 12 | const scrollToPageSection = (event, sectionId) => { 13 | if (event) event.preventDefault() 14 | const targetEl = document.getElementById(sectionId); 15 | if (targetEl) targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' }) 16 | } 17 | 18 | const LegalPageNavigation = (props) => { 19 | const { 20 | activeSection, 21 | sections, 22 | } = props 23 | const [navOpen, setNavOpen] = useState(false) 24 | const sectionTitles = sections && sections.map(section => section.sectionHeading.text) 25 | useEffect(() => { 26 | const navChecker = async () => { 27 | if (navOpen) setNavOpen(false) 28 | } 29 | if (isClient) window.addEventListener('scroll', navChecker) 30 | return () => { 31 | if (isClient) window.addEventListener('scroll', navChecker) 32 | } 33 | }) 34 | return ( 35 | 98 | ) 99 | } 100 | 101 | export default LegalPageNavigation; 102 | -------------------------------------------------------------------------------- /src/components/legal/LegalPageHero.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx, Styled } from 'theme-ui'; 3 | import { Link } from 'gatsby'; 4 | 5 | 6 | const LegalPageHero = ({ title, siteName, homePath }) => ( 7 |
    15 |
    24 |
    41 | {siteName && ( 42 | 51 | {siteName} 52 | 53 | )} 54 | 61 | {title} 62 | 63 | 70 | 77 | Back to home 78 | 79 | 80 |
    81 |
    99 |
    117 |
    118 |
    119 | ) 120 | 121 | export default LegalPageHero 122 | -------------------------------------------------------------------------------- /src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import theme from './theme'; 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | /* http://meyerweb.com/eric/tools/css/reset/ 6 | v2.0 | 20110126 7 | License: none (public domain) 8 | */ 9 | html, body, div, span, applet, object, iframe, 10 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 11 | a, abbr, acronym, address, big, cite, code, 12 | del, dfn, em, img, ins, kbd, q, s, samp, 13 | small, strike, strong, sub, sup, tt, var, 14 | b, u, i, center, 15 | dl, dt, dd, ol, ul, li, 16 | fieldset, form, label, legend, 17 | article, aside, canvas, details, embed, 18 | figure, figcaption, footer, header, hgroup, 19 | menu, nav, output, ruby, section, summary, 20 | time, mark, audio, video { 21 | margin: 0; 22 | padding: 0; 23 | border: 0; 24 | font-size: 100%; 25 | font: inherit; 26 | vertical-align: baseline; 27 | } 28 | article, aside, details, figcaption, figure, 29 | footer, header, hgroup, menu, nav, section { 30 | display: block; 31 | } 32 | @media screen and (min-width: 35em) { 33 | html { 34 | margin-right: calc(-100vw + 100%); 35 | overflow-x: hidden; 36 | } 37 | } 38 | ol, ul, li { 39 | list-style: none; 40 | } 41 | blockquote, q { 42 | quotes: none; 43 | } 44 | blockquote::before, blockquote::after, 45 | q::before, q::after { 46 | content: ''; 47 | content: none; 48 | } 49 | table { 50 | border-collapse: collapse; 51 | border-spacing: 0; 52 | } 53 | * { 54 | box-sizing: border-box; 55 | } 56 | body { 57 | font-size: 100%; 58 | font-variant-ligatures: none; 59 | text-rendering: optimizeLegibility; 60 | text-shadow: rgba(0, 0, 0, .01) 0 0 1px; 61 | } 62 | h1, 63 | h2, 64 | h3, 65 | h4, 66 | h5, 67 | h6 { 68 | 69 | } 70 | img { 71 | display: block; 72 | width: 100%; 73 | height: auto; 74 | } 75 | button, 76 | input { 77 | font-family: inherit; 78 | font-size: inherit; 79 | background: none; 80 | border: none; 81 | outline: none; 82 | appearance: none; 83 | border-radius: 0; 84 | padding: 0; 85 | text-align: left; 86 | resize: none; 87 | &:focus { 88 | outline: none; 89 | } 90 | &:invalid { 91 | box-shadow: none; 92 | } 93 | } 94 | /* Added to Fix Footer to bottom of viewport */ 95 | html, body { 96 | height: 100%; 97 | } 98 | .siteRoot { 99 | padding: 60px 0 0 0; 100 | margin: 0 auto; 101 | display: flex; 102 | min-height: 100vh; 103 | flex-direction: column; 104 | } 105 | .siteContent { 106 | flex: 1; 107 | } 108 | /* Added to prevent scrolling when the menu is open */ 109 | .contain { 110 | overflow: hidden; 111 | } 112 | button, 113 | input, 114 | textarea, 115 | select { 116 | color: ${theme.colors.text}; 117 | font-family: inherit; 118 | font-size: inherit; 119 | background: none; 120 | border: none; 121 | appearance: none; 122 | /* stylelint-disable-next-line */ 123 | -webkit-appearance: none; 124 | /* stylelint-disable-next-line */ 125 | -moz-appearance: none; 126 | border-radius: 0; 127 | resize: none; 128 | &:invalid { 129 | box-shadow: none; 130 | } 131 | &:focus { 132 | outline: 5px auto #5E9ED6; 133 | outline: 5px auto -webkit-focus-ring-color; 134 | } 135 | } 136 | body:not(.user-is-tabbing) button:focus, 137 | body:not(.user-is-tabbing) input:focus, 138 | body:not(.user-is-tabbing) select:focus, 139 | body:not(.user-is-tabbing) textarea:focus, 140 | body:not(.user-is-tabbing) a:focus { 141 | outline: none; 142 | } 143 | `; 144 | export default GlobalStyle; 145 | -------------------------------------------------------------------------------- /src/templates/legal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { graphql } from 'gatsby'; 3 | import { Layout } from '../components/common'; 4 | import { 5 | LegalPageHero, 6 | LegalPageBody, 7 | } from '../components/legal'; 8 | import { Styled } from 'theme-ui'; 9 | 10 | const arraysEqual = (arr1, arr2) => { 11 | if (arr1.length !== arr2.length) 12 | return false 13 | for (var i = arr1.length; i--;) { 14 | if(arr1[i] !== arr2[i]) 15 | return false 16 | } 17 | return true 18 | } 19 | 20 | 21 | class LegalPageTemplate extends Component { 22 | state = { 23 | sectionsInView: [], 24 | } 25 | 26 | sectionInViewHandler = ( sectionIndex, isInView ) => { 27 | const { sectionsInView } = this.state 28 | let newSectionsInView = [ ...sectionsInView ] 29 | const indexExists = newSectionsInView.includes(sectionIndex) 30 | if (isInView) { 31 | // Intersection Observer has notified us section has come into view 32 | // indexExists prevents us from adding duplicates 33 | if (!indexExists) newSectionsInView = [ 34 | ...sectionsInView, 35 | sectionIndex, 36 | ] 37 | } else { 38 | // Intersection Observer has notified us section is now out of view 39 | newSectionsInView = sectionsInView.filter(index => index !== sectionIndex) 40 | } 41 | if (!arraysEqual(sectionsInView, newSectionsInView)) { 42 | // Only if a new section has come into, or an existing section has come 43 | // out of view, we update the state. 44 | this.setState({ 45 | sectionsInView: newSectionsInView, 46 | }) 47 | } 48 | 49 | } 50 | 51 | render() { 52 | const { 53 | sectionsInView, 54 | } = this.state 55 | // The 'active' section is the section closest to the top of the page that 56 | // are still in view (therefore, the smallest index in our array) 57 | const activeSection = sectionsInView.length > 0 ? sectionsInView.reduce((a, b) => Math.min(a, b)) : 0 58 | const { 59 | data: { 60 | site: { 61 | siteMetadata: { 62 | homePath = '/', 63 | siteName, 64 | }, 65 | }, 66 | page: { 67 | data: pageData, 68 | }, 69 | }, 70 | location, 71 | } = this.props 72 | const { 73 | pageTitle, 74 | heroSubtitle, 75 | sections, 76 | metaTitle, 77 | metaDescription, 78 | openGraphImage, 79 | } = pageData; 80 | const seoData = { 81 | metaTitle, 82 | metaDescription, 83 | openGraphImage, 84 | }; 85 | const bannerTitle = pageTitle && pageTitle.text ? pageTitle.text : 'P'; 86 | const bannerSubtitle = heroSubtitle && heroSubtitle.text ? heroSubtitle.text : 'You have questions, we have answers'; 87 | return ( 88 | 89 | 93 | 99 | 104 | 105 | 106 | ) 107 | } 108 | } 109 | 110 | export default LegalPageTemplate 111 | 112 | export const pageQuery = graphql` 113 | query LegalPageBySlug($uid: String!) { 114 | site { 115 | siteMetadata { 116 | siteName, 117 | homePath, 118 | } 119 | }, 120 | page: prismicLegal(uid: { eq: $uid }) { 121 | data { 122 | pageTitle: page_name { 123 | text 124 | } 125 | heroSubtitle: hero_subtitle { 126 | text 127 | } 128 | sections { 129 | content { 130 | html 131 | } 132 | sectionHeading: section_heading { 133 | text 134 | } 135 | } 136 | metaTitle: meta_title { 137 | html 138 | text 139 | }, 140 | metaDescription: meta_description { 141 | html 142 | text 143 | }, 144 | openGraphImage: open_graph_image { 145 | alt 146 | copyright 147 | url 148 | } 149 | } 150 | } 151 | } 152 | ` 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Mockups of gatsby-theme-legals-prismic in action](https://raw.githubusercontent.com/AllanPooley/gatsby-theme-legals-demo/master/src/assets/images/gatsby-theme-legals-prismic-mockup.jpg) 2 | 3 | # Gatsby Theme Legals Prismic 4 | 5 | - [Gatsby Theme](https://www.gatsbyjs.org/docs/themes/what-are-gatsby-themes/) for adding polished legal pages 💅out-of-the-box. 6 | - Responsive across Mobiles 📱, Tablets 💊 and Desktops 🖥️ 7 | - Customisable to your brand using [Theme UI](https://theme-ui.com/) 🎨 8 | - Builds legal pages sourced from content in [Prismic](https://prismic.io/) 9 | - Demo at [https://gatsby-theme-legals.netlify.com/](https://gatsby-theme-legals.netlify.com/) 10 | 11 | ## Why? 12 | 13 | Legal pages are probably the most unexciting part of your site, and the last place you want to expend your creative energy. 14 | 15 | The purpose of `gatsby-theme-legals` is to do the heavy lifting for you. Super polished, responsive legal pages that you can just plug onto your existing project. 16 | 17 | ## Installation 18 | 19 | ``` 20 | yarn add @littleplusbig/gatsby-theme-legals-prismic 21 | ``` 22 | 23 | ## Configuration 24 | 25 | In your `gatsby-config.js`, under `plugins` add: 26 | 27 | ``` 28 | { 29 | resolve: "gatsby-theme-legals-prismic", 30 | options: { 31 | prismicRepositoryName: PRISMIC_REPO_NAME, 32 | prismicAccessToken: PRISMIC_API_KEY, 33 | siteName: YOUR_SITE_NAME, // (Optional) 34 | homePath: HOME_PATH // (Optional) Defaults to '/' 35 | }, 36 | }, 37 | ``` 38 | 39 | Replacing `PRISMIC_REPO_NAME`, `PRISMIC_API_KEY`, `YOUR_SITE_NAME` and `HOME_PATH` with their respective values. 40 | 41 | ## Prismic Configuration 42 | 43 | 1. Create a new custom type in your Prismic repository. 44 | 2. Make sure that it is repeatable and name it `Legal`. 45 | 3. Using the JSON Editor paste in the following content structure: 46 | 47 | ``` 48 | { 49 | "Main": { 50 | "page_name": { 51 | "type": "StructuredText", 52 | "config": { 53 | "single": "heading1", 54 | "label": "Page Name", 55 | "placeholder": "Privacy Policy" 56 | } 57 | }, 58 | "uid": { 59 | "type": "UID", 60 | "config": { 61 | "label": "Slug", 62 | "placeholder": "privacy-policy" 63 | } 64 | }, 65 | "hero_subtitle": { 66 | "type": "StructuredText", 67 | "config": { 68 | "single": "paragraph", 69 | "label": "Hero Subtitle", 70 | "placeholder": "How we manage your data" 71 | } 72 | }, 73 | "sections": { 74 | "type": "Group", 75 | "config": { 76 | "fields": { 77 | "section_heading": { 78 | "type": "StructuredText", 79 | "config": { 80 | "single": "heading2", 81 | "label": "Section Heading", 82 | "placeholder": "General information" 83 | } 84 | }, 85 | "content": { 86 | "type": "StructuredText", 87 | "config": { 88 | "multi": "paragraph, preformatted, heading3, strong, em, hyperlink, list-item, o-list-item, o-list-item", 89 | "allowTargetBlank": true, 90 | "label": "Content", 91 | "placeholder": "Information on this website is of a general nature. Our company has ..." 92 | } 93 | } 94 | }, 95 | "label": "Sections" 96 | } 97 | } 98 | }, 99 | "SEO": { 100 | "meta_title": { 101 | "type": "StructuredText", 102 | "config": { 103 | "single": "heading1", 104 | "label": "Meta Title", 105 | "placeholder": "Enter meta title" 106 | } 107 | }, 108 | "meta_description": { 109 | "type": "StructuredText", 110 | "config": { 111 | "single": "paragraph", 112 | "label": "Meta Description", 113 | "placeholder": "Enter meta description" 114 | } 115 | }, 116 | "open_graph_image": { 117 | "type": "Image", 118 | "config": { 119 | "constraint": { 120 | "width": 1200, 121 | "height": 630 122 | }, 123 | "thumbnails": [], 124 | "label": "Open Graph Image" 125 | } 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | 4. Create one or more `Legal` Content pages, each with 1 or more sections. Don't forget to populate each page's `SEO` tab! 132 | 133 | ## Laying Down the Law 134 | 135 | If you don't already have a Privacy Policy or Terms and Conditions document, you can generate one at [Iubenda](https://www.iubenda.com/). 136 | 137 | ## Overriding the Theme 138 | 139 | ### Colors and Styles 140 | 141 | This project uses [theme-ui](https://theme-ui.com/), allowing some of the styling to be customised to your project's brand. 142 | 143 | In order to override the styles, in the `src` directory of your project, add a folder titled `gatsby-plugin-theme-ui`, and within that folder a file named `index.js`. 144 | 145 | Inside of this file (`your-gatsby-project/src/gatsby-plugin-theme-ui/index.js`) add the following: 146 | 147 | ``` 148 | import baseTheme from '@littleplusbig/gatsby-theme-legals-prismic/src/gatsby-plugin-theme-ui'; 149 | 150 | export default { 151 | ...baseTheme, 152 | fonts: { 153 | ...baseTheme.fonts, 154 | body: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif', 155 | heading: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif', 156 | }, 157 | colors: { 158 | ...baseTheme.colors, 159 | text: '#333333', 160 | background: '#FFFFFF', 161 | primary: '#6F2B9F', 162 | primaryDark: '#5B2589', 163 | primaryLight: '#BB75D1', 164 | white: '#FFFFFF', 165 | offWhite: '#FCFAFF', 166 | black: '#000000', 167 | offBlack: '#333333', 168 | grey: '#F3F3F3', 169 | }, 170 | }; 171 | 172 | ``` 173 | 174 | Above are the default values for the theme, which you can change depending on your project. 175 | 176 | In particular, the colours accenting each legal page are controlled by `primary`, `primaryLight` and `primaryDark`. 177 | 178 | For example, here is how I might change the theme colours from shades of purple, to a snazzy blue: 179 | 180 | ``` 181 | import baseTheme from '@littleplusbig/gatsby-theme-legals-prismic/src/gatsby-plugin-theme-ui'; 182 | 183 | export default { 184 | ...baseTheme, 185 | colors: { 186 | ...baseTheme.colors, 187 | primary: '#7ed6df', 188 | primaryDark: '#22a6b3', 189 | primaryLight: '#c7ecee', 190 | }, 191 | }; 192 | 193 | ``` 194 | 195 | ![An example of a theme color change](https://raw.githubusercontent.com/AllanPooley/gatsby-theme-legals-demo/master/src/assets/images/gatsby-theme-legals-prismic-mockup-color-change.jpg) 196 | 197 | The complete set of customisable theme values can be explored in [gatsby-theme-legals-prismic/src/styles/theme.js](https://github.com/littleplusbig/gatsby-theme-legals-prismic/blob/master/src/styles/theme.js) 198 | 199 | More information about `gatsby-plugin-theme-ui` [here](https://www.npmjs.com/package/gatsby-plugin-theme-ui). 200 | 201 | ### Components 202 | 203 | The components that make up the legal pages can be some what customised too. This can be done through concept new to Gatsby Themes called '[Component Shadowing](https://www.gatsbyjs.org/blog/2019-04-29-component-shadowing/)'. 204 | 205 | If you wish to override a component, in the `src` directory of your project, create the following directory structure: `@littleplusbig/gatsby-theme-legals-prismic/components`. 206 | 207 | There are several components that a legal page, they can all be viewed here: [gatsby-theme-legals-prismic/src/components](https://github.com/littleplusbig/gatsby-theme-legals-prismic/tree/master/src/components) 208 | 209 | An example of how these components might be customised is adding your project's `
    ` and `