From 5a3bf52ebb0085d56112abb40c39ba0417f646f2 Mon Sep 17 00:00:00 2001 From: Waylon Walker Date: Thu, 9 May 2019 10:15:41 -0500 Subject: [PATCH] Initial commit --- .editorconfig | 8 + .gitignore | 12 ++ README.md | 38 +++++ lerna.json | 7 + package.json | 20 +++ studio/.eslintignore | 1 + studio/.eslintrc.js | 16 ++ studio/.gitignore | 6 + studio/.prettierrc | 4 + studio/README.md | 1 + studio/config/.checksums | 6 + studio/config/@sanity/data-aspects.json | 3 + studio/config/@sanity/default-layout.json | 6 + studio/config/@sanity/default-login.json | 7 + studio/dashboardConfig.js | 53 ++++++ studio/deskStructure.js | 36 ++++ studio/netlify.toml | 4 + studio/package.json | 43 +++++ .../sanity.json | 13 ++ .../src/components/StructureMenuWidget.css | 40 +++++ .../src/components/StructureMenuWidget.js | 39 +++++ .../src/components/index.js | 1 + .../src/lib/structure.js | 99 +++++++++++ .../src/props.js | 11 ++ .../src/widget.js | 10 ++ studio/sanity.json | 35 ++++ studio/schemas/documents/category.js | 17 ++ studio/schemas/documents/person.js | 41 +++++ studio/schemas/documents/project.js | 90 ++++++++++ studio/schemas/documents/siteSettings.js | 35 ++++ studio/schemas/objects/bioPortableText.js | 34 ++++ studio/schemas/objects/figure.js | 34 ++++ studio/schemas/objects/projectMember.js | 42 +++++ studio/schemas/objects/projectPortableText.js | 51 ++++++ studio/schemas/objects/simplePortableText.js | 39 +++++ studio/schemas/schema.js | 41 +++++ studio/static/.gitkeep | 1 + studio/static/favicon.ico | Bin 0 -> 1150 bytes web/.env.development.template | 6 + web/.env.production | 2 + web/.eslintrc.js | 13 ++ web/.gitignore | 75 +++++++++ web/.prettierrc | 3 + web/README.md | 1 + web/client-config.js | 6 + web/gatsby-browser.js | 7 + web/gatsby-config.js | 25 +++ web/gatsby-node.js | 51 ++++++ web/gatsby-ssr.js | 7 + web/package.json | 42 +++++ web/postcss.config.js | 8 + web/src/components/block-content.js | 10 ++ web/src/components/block-text.js | 7 + web/src/components/container.js | 9 + web/src/components/container.module.css | 12 ++ web/src/components/figure.js | 21 +++ web/src/components/figure.module.css | 11 ++ web/src/components/graphql-error-list.js | 12 ++ web/src/components/header.js | 30 ++++ web/src/components/header.module.css | 114 +++++++++++++ web/src/components/icon/hamburger.js | 20 +++ web/src/components/icon/index.js | 13 ++ web/src/components/layout.js | 23 +++ web/src/components/layout.module.css | 48 ++++++ web/src/components/project-preview-grid.js | 34 ++++ .../project-preview-grid.module.css | 51 ++++++ web/src/components/project-preview.js | 34 ++++ web/src/components/project-preview.module.css | 42 +++++ web/src/components/project.js | 76 +++++++++ web/src/components/project.module.css | 98 +++++++++++ web/src/components/role-list.js | 55 ++++++ web/src/components/role-list.module.css | 46 ++++++ web/src/components/seo.js | 96 +++++++++++ web/src/components/serializers.js | 9 + web/src/components/typography.module.css | 156 ++++++++++++++++++ web/src/containers/layout.js | 44 +++++ web/src/lib/helpers.js | 33 ++++ web/src/lib/image-url.js | 8 + web/src/lib/string-utils.js | 3 + web/src/pages/404.js | 14 ++ web/src/pages/archive.js | 61 +++++++ web/src/pages/index.js | 103 ++++++++++++ web/src/styles/custom-media.css | 4 + web/src/styles/custom-properties.css | 29 ++++ web/src/styles/layout.css | 21 +++ web/src/templates/project.js | 102 ++++++++++++ 86 files changed, 2639 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lerna.json create mode 100644 package.json create mode 100644 studio/.eslintignore create mode 100644 studio/.eslintrc.js create mode 100644 studio/.gitignore create mode 100644 studio/.prettierrc create mode 100644 studio/README.md create mode 100644 studio/config/.checksums create mode 100644 studio/config/@sanity/data-aspects.json create mode 100644 studio/config/@sanity/default-layout.json create mode 100644 studio/config/@sanity/default-login.json create mode 100644 studio/dashboardConfig.js create mode 100644 studio/deskStructure.js create mode 100644 studio/netlify.toml create mode 100644 studio/package.json create mode 100644 studio/plugins/dashboard-widget-structure-menu/sanity.json create mode 100644 studio/plugins/dashboard-widget-structure-menu/src/components/StructureMenuWidget.css create mode 100644 studio/plugins/dashboard-widget-structure-menu/src/components/StructureMenuWidget.js create mode 100644 studio/plugins/dashboard-widget-structure-menu/src/components/index.js create mode 100644 studio/plugins/dashboard-widget-structure-menu/src/lib/structure.js create mode 100644 studio/plugins/dashboard-widget-structure-menu/src/props.js create mode 100644 studio/plugins/dashboard-widget-structure-menu/src/widget.js create mode 100644 studio/sanity.json create mode 100644 studio/schemas/documents/category.js create mode 100644 studio/schemas/documents/person.js create mode 100644 studio/schemas/documents/project.js create mode 100644 studio/schemas/documents/siteSettings.js create mode 100644 studio/schemas/objects/bioPortableText.js create mode 100644 studio/schemas/objects/figure.js create mode 100644 studio/schemas/objects/projectMember.js create mode 100644 studio/schemas/objects/projectPortableText.js create mode 100644 studio/schemas/objects/simplePortableText.js create mode 100644 studio/schemas/schema.js create mode 100644 studio/static/.gitkeep create mode 100644 studio/static/favicon.ico create mode 100644 web/.env.development.template create mode 100644 web/.env.production create mode 100644 web/.eslintrc.js create mode 100644 web/.gitignore create mode 100644 web/.prettierrc create mode 100644 web/README.md create mode 100644 web/client-config.js create mode 100644 web/gatsby-browser.js create mode 100644 web/gatsby-config.js create mode 100644 web/gatsby-node.js create mode 100644 web/gatsby-ssr.js create mode 100644 web/package.json create mode 100644 web/postcss.config.js create mode 100644 web/src/components/block-content.js create mode 100644 web/src/components/block-text.js create mode 100644 web/src/components/container.js create mode 100644 web/src/components/container.module.css create mode 100644 web/src/components/figure.js create mode 100644 web/src/components/figure.module.css create mode 100644 web/src/components/graphql-error-list.js create mode 100644 web/src/components/header.js create mode 100644 web/src/components/header.module.css create mode 100644 web/src/components/icon/hamburger.js create mode 100644 web/src/components/icon/index.js create mode 100644 web/src/components/layout.js create mode 100644 web/src/components/layout.module.css create mode 100644 web/src/components/project-preview-grid.js create mode 100644 web/src/components/project-preview-grid.module.css create mode 100644 web/src/components/project-preview.js create mode 100644 web/src/components/project-preview.module.css create mode 100644 web/src/components/project.js create mode 100644 web/src/components/project.module.css create mode 100644 web/src/components/role-list.js create mode 100644 web/src/components/role-list.module.css create mode 100644 web/src/components/seo.js create mode 100644 web/src/components/serializers.js create mode 100644 web/src/components/typography.module.css create mode 100644 web/src/containers/layout.js create mode 100644 web/src/lib/helpers.js create mode 100644 web/src/lib/image-url.js create mode 100644 web/src/lib/string-utils.js create mode 100644 web/src/pages/404.js create mode 100644 web/src/pages/archive.js create mode 100644 web/src/pages/index.js create mode 100644 web/src/styles/custom-media.css create mode 100644 web/src/styles/custom-properties.css create mode 100644 web/src/styles/layout.css create mode 100644 web/src/templates/project.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1923d41 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9f11c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Mac files +.DS_Store + +# Dependency directories +/node_modules + +# lerna files +/lerna-debug.log + +# Lock files +/package-lock.json +/yarn.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..48401e2 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# TestGatsbySanity + +_A portfolio using structured content and a static site builder._ + +Deployed from [sanity.io/create](https://www.sanity.io/create/?template=sanity-io%2Fsanity-template-gatsby-portfolio). + +## What you have + +- A blazing fast portfolio with [Gatsby.js](https://gatsbyjs.org) +- Structured content using [Sanity.io](https://www.sanity.io) +- Global deployment on [Netlify](https://netlify.com) + +## Quick start + +1. Clone this repository from your GitHub account +2. `npm install` in the project root folder on local +3. `npm run dev` to start the studio and frontend locally + - Your studio should be running on [http://localhost:3333](http://localhost:3333) + - Your frontend should be running on [http://localhost:8000](http://localhost:8000) +4. `npm run build` to build to production locally + +## Enable real-time content preview on development + +1. Go to your [project’s API settings on manage.sanity.io](https://manage.sanity.io/projects/dkwlsoid/settings/api) and create a token with read rights. +2. Rename `.env.development.tenplate` to `.env.development` and paste in the token: `SANITY_READ_TOKEN="yourTokenHere"`. +3. Restart the development server (`ctrl + C` and `npm run dev`). + +If you want to turn off preview you can set `watchMode: false` in gatsby-config.js. If you just want to preview published changes you can set `overlayDrafts: false` in gatsby-config.js. + +## Deploy changes + +Netlify automatically deploys new changes commited to master on GitHub. If you want to change deployment branch, do so in [build & deploy settings on Netlify](https://www.netlify.com/docs/continuous-deployment/#branches-deploys). + +## Stuck? Get help + +[![Slack Community Button](https://slack.sanity.io/badge.svg)](https://slack.sanity.io/) + +Join [Sanity’s developer community](https://slack.sanity.io) or ping us [on twitter](https://twitter.com/sanity_io). diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..a7646eb --- /dev/null +++ b/lerna.json @@ -0,0 +1,7 @@ +{ + "packages": [ + "web", + "studio" + ], + "version": "1.0.0" +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0587db6 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "name": "TestGatsbySanity", + "version": "0.0.0", + "scripts": { + "build": "lerna run build --parallel", + "dev": "lerna run dev --parallel", + "format": "lerna run format", + "build-studio": "(cd studio && npm run build)", + "build-web": "(cd studio && SANITY_AUTH_TOKEN=$SANITY_DEPLOY_STUDIO_TOKEN npm run graphql-deploy) && (cd web && npm run build)", + "graphql-deploy": "lerna run graphql-deploy", + "lint": "lerna run lint", + "postinstall": "lerna bootstrap", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "devDependencies": { + "@sanity/cli": "^0.140.17", + "lerna": "^3.13.1" + } +} \ No newline at end of file diff --git a/studio/.eslintignore b/studio/.eslintignore new file mode 100644 index 0000000..a261f29 --- /dev/null +++ b/studio/.eslintignore @@ -0,0 +1 @@ +dist/* diff --git a/studio/.eslintrc.js b/studio/.eslintrc.js new file mode 100644 index 0000000..29f08f9 --- /dev/null +++ b/studio/.eslintrc.js @@ -0,0 +1,16 @@ +const path = require('path') + +module.exports = { + extends: ['standard', 'standard-react'], + parser: 'babel-eslint', + rules: { + 'react/prop-types': 0, + 'object-curly-spacing': ['error', 'never'] + }, + settings: { + react: { + pragma: 'React', + version: '16.8.6' + } + } +} diff --git a/studio/.gitignore b/studio/.gitignore new file mode 100644 index 0000000..7398513 --- /dev/null +++ b/studio/.gitignore @@ -0,0 +1,6 @@ +/dist +/node_modules + +# Lock files +/package-lock.json +/yarn.lock diff --git a/studio/.prettierrc b/studio/.prettierrc new file mode 100644 index 0000000..5ac85e2 --- /dev/null +++ b/studio/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 100, + "singleQuote": true +} diff --git a/studio/README.md b/studio/README.md new file mode 100644 index 0000000..e2093dc --- /dev/null +++ b/studio/README.md @@ -0,0 +1 @@ +# TestGatsbySanity-studio diff --git a/studio/config/.checksums b/studio/config/.checksums new file mode 100644 index 0000000..16e99b9 --- /dev/null +++ b/studio/config/.checksums @@ -0,0 +1,6 @@ +{ + "#": "Used by Sanity to keep track of configuration file checksums, do not delete or modify!", + "@sanity/default-layout": "bb034f391ba508a6ca8cd971967cbedeb131c4d19b17b28a0895f32db5d568ea", + "@sanity/default-login": "6fb6d3800aa71346e1b84d95bbcaa287879456f2922372bb0294e30b968cd37f", + "@sanity/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6" +} diff --git a/studio/config/@sanity/data-aspects.json b/studio/config/@sanity/data-aspects.json new file mode 100644 index 0000000..53429ef --- /dev/null +++ b/studio/config/@sanity/data-aspects.json @@ -0,0 +1,3 @@ +{ + "listOptions": {} +} \ No newline at end of file diff --git a/studio/config/@sanity/default-layout.json b/studio/config/@sanity/default-layout.json new file mode 100644 index 0000000..a0d35bf --- /dev/null +++ b/studio/config/@sanity/default-layout.json @@ -0,0 +1,6 @@ +{ + "toolSwitcher": { + "order": [], + "hidden": [] + } +} \ No newline at end of file diff --git a/studio/config/@sanity/default-login.json b/studio/config/@sanity/default-login.json new file mode 100644 index 0000000..7fad17f --- /dev/null +++ b/studio/config/@sanity/default-login.json @@ -0,0 +1,7 @@ +{ + "providers": { + "mode": "append", + "redirectOnSingle": false, + "entries": [] + } +} \ No newline at end of file diff --git a/studio/dashboardConfig.js b/studio/dashboardConfig.js new file mode 100644 index 0000000..eec9a8b --- /dev/null +++ b/studio/dashboardConfig.js @@ -0,0 +1,53 @@ +export default { + widgets: [ + { + name: 'sanity-tutorials', + options: { + templateRepoId: 'sanity-io/sanity-template-gatsby-portfolio' + } + }, + {name: 'structure-menu'}, + { + name: 'project-info', + options: { + __experimental_before: [ + { + name: 'netlify', + options: { + description: + 'NOTE: Because these sites are static builds, they need to be re-deployed to see the changes when documents are published.', + sites: [ + { + buildHookId: '5cd4441832c8cca9fab7c1b2', + title: 'Sanity Studio', + name: 'TestGatsbySanity-studio', + apiId: '322eb270-ce44-446b-989e-a2000fa7acab' + }, + { + buildHookId: '5cd44418d31f16c9c00451cd', + title: 'Portfolio Website', + name: 'TestGatsbySanity', + apiId: '24555f21-7e83-43f5-8a76-82dec0822171' + } + ] + } + } + ], + data: [ + { + title: 'GitHub repo', + value: 'https://github.com/WaylonWalker/TestGatsbySanity', + category: 'Code' + }, + {title: 'Frontend', value: 'https://TestGatsbySanity.netlify.com', category: 'apps'} + ] + } + }, + {name: 'project-users', layout: {height: 'auto'}}, + { + name: 'document-list', + options: {title: 'Recent projects', order: '_createdAt desc', types: ['project']}, + layout: {width: 'medium'} + } + ] +} diff --git a/studio/deskStructure.js b/studio/deskStructure.js new file mode 100644 index 0000000..458d2f7 --- /dev/null +++ b/studio/deskStructure.js @@ -0,0 +1,36 @@ +import S from '@sanity/desk-tool/structure-builder' +import MdSettings from 'react-icons/lib/md/settings' + +const hiddenDocTypes = listItem => + !['category', 'person', 'project', 'siteSettings'].includes(listItem.getId()) + +export default () => + S.list() + .title('Content') + .items([ + S.listItem() + .title('Settings') + .child( + S.editor() + .id('siteSettings') + .schemaType('siteSettings') + .documentId('siteSettings') + ) + .icon(MdSettings), + S.listItem() + .title('Projects') + .schemaType('project') + .child(S.documentTypeList('project').title('Projects')), + S.listItem() + .title('People') + .schemaType('person') + .child(S.documentTypeList('person').title('People')), + S.listItem() + .title('Categories') + .schemaType('category') + .child(S.documentTypeList('category').title('Categories')), + // This returns an array of all the document types + // defined in schema.js. We filter out those that we have + // defined the structure above + ...S.documentTypeListItems().filter(hiddenDocTypes) + ]) diff --git a/studio/netlify.toml b/studio/netlify.toml new file mode 100644 index 0000000..1cb2010 --- /dev/null +++ b/studio/netlify.toml @@ -0,0 +1,4 @@ +[[redirects]] + from = "/*" + to = "/" + status = 200 diff --git a/studio/package.json b/studio/package.json new file mode 100644 index 0000000..eb91f14 --- /dev/null +++ b/studio/package.json @@ -0,0 +1,43 @@ +{ + "private": true, + "name": "TestGatsbySanity-studio", + "version": "1.0.0", + "main": "package.json", + "author": "Sanity ", + "scripts": { + "dev": "sanity start", + "format": "prettier-eslint --write \"**/*.js\" \"!node_modules/**\"", + "build": "sanity build", + "graphql-deploy": "sanity graphql deploy --playground", + "lint": "eslint .", + "test": "sanity check" + }, + "dependencies": { + "@sanity/base": "^0.140.17", + "@sanity/cli": "^0.140.17", + "@sanity/components": "^0.140.20", + "@sanity/core": "^0.140.20", + "@sanity/dashboard": "^0.140.19", + "@sanity/default-layout": "^0.140.20", + "@sanity/default-login": "^0.140.15", + "@sanity/desk-tool": "^0.140.20", + "date-fns": "^1.30.1", + "prop-types": "^15.7.2", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "sanity-plugin-dashboard-widget-document-list": "^0.0.8", + "sanity-plugin-dashboard-widget-netlify": "^1.0.0" + }, + "devDependencies": { + "babel-eslint": "^10.0.1", + "eslint": "^5.16.0", + "eslint-config-standard": "^12.0.0", + "eslint-config-standard-react": "^7.0.2", + "eslint-plugin-import": "^2.17.2", + "eslint-plugin-node": "^9.0.1", + "eslint-plugin-promise": "^4.1.1", + "eslint-plugin-react": "^7.13.0", + "eslint-plugin-standard": "^4.0.0", + "prettier-eslint-cli": "^4.7.1" + } +} \ No newline at end of file diff --git a/studio/plugins/dashboard-widget-structure-menu/sanity.json b/studio/plugins/dashboard-widget-structure-menu/sanity.json new file mode 100644 index 0000000..b53073c --- /dev/null +++ b/studio/plugins/dashboard-widget-structure-menu/sanity.json @@ -0,0 +1,13 @@ +{ + "paths": { + "source": "./src", + "compiled": "./lib" + }, + "parts": [ + { + "name": "part:@sanity/dashboard/widget/create", + "implements": "part:@sanity/dashboard/widget", + "path": "widget.js" + } + ] +} \ No newline at end of file diff --git a/studio/plugins/dashboard-widget-structure-menu/src/components/StructureMenuWidget.css b/studio/plugins/dashboard-widget-structure-menu/src/components/StructureMenuWidget.css new file mode 100644 index 0000000..f83dcfc --- /dev/null +++ b/studio/plugins/dashboard-widget-structure-menu/src/components/StructureMenuWidget.css @@ -0,0 +1,40 @@ +@import 'part:@sanity/base/theme/variables-style'; + +.root { + composes: container from "part:@sanity/dashboard/widget-styles"; +} + +.header { + composes: header from "part:@sanity/dashboard/widget-styles"; +} + +.title { + composes: title from "part:@sanity/dashboard/widget-styles"; +} + +.content { + display: grid; + padding: var(--small-padding); + grid-gap: var(--small-padding); + grid-template-columns: 1fr 1fr; + overflow-x: auto; + border-top: 1px solid var(--hairline-color); + + @media (--screen-medium) { + grid-template-columns: 1fr 1fr 1fr 1fr; + } +} + +.link { + composes: item from 'part:@sanity/base/theme/layout/selectable-style'; + display: block; + border-radius: 2px; + padding: var(--small-padding); + text-decoration: none; + text-align: center; + box-sizing: border-box; +} + +.iconWrapper { + font-size: 2em; +} diff --git a/studio/plugins/dashboard-widget-structure-menu/src/components/StructureMenuWidget.js b/studio/plugins/dashboard-widget-structure-menu/src/components/StructureMenuWidget.js new file mode 100644 index 0000000..b906830 --- /dev/null +++ b/studio/plugins/dashboard-widget-structure-menu/src/components/StructureMenuWidget.js @@ -0,0 +1,39 @@ +import {Link} from 'part:@sanity/base/router' +import FolderIcon from 'part:@sanity/base/folder-icon' +import FileIcon from 'part:@sanity/base/file-icon' +import React from 'react' +import styles from './StructureMenuWidget.css' + +function getIconComponent (item) { + if (item.icon) return item.icon + if (!item.schemaType) return FileIcon + return item.schemaType.icon || FolderIcon +} + +function StructureMenuWidget (props) { + return ( +
+
+

Edit your content

+
+ +
+ {props.structure.items.map(item => { + const Icon = getIconComponent(item) + return ( +
+ +
+ +
+
{item.title}
+ +
+ ) + })} +
+
+ ) +} + +export default StructureMenuWidget diff --git a/studio/plugins/dashboard-widget-structure-menu/src/components/index.js b/studio/plugins/dashboard-widget-structure-menu/src/components/index.js new file mode 100644 index 0000000..60e0038 --- /dev/null +++ b/studio/plugins/dashboard-widget-structure-menu/src/components/index.js @@ -0,0 +1 @@ +export {default as StructureMenuWidget} from './StructureMenuWidget' diff --git a/studio/plugins/dashboard-widget-structure-menu/src/lib/structure.js b/studio/plugins/dashboard-widget-structure-menu/src/lib/structure.js new file mode 100644 index 0000000..1c7af3e --- /dev/null +++ b/studio/plugins/dashboard-widget-structure-menu/src/lib/structure.js @@ -0,0 +1,99 @@ +/* global __DEV__ */ + +import {defer, from as observableFrom, of as observableOf, throwError} from 'rxjs' +import {mergeMap} from 'rxjs/operators' + +// eslint-disable-next-line import/no-commonjs +const {StructureBuilder} = require('@sanity/structure') + +let prevStructureError = null +if (__DEV__) { + if (module.hot && module.hot.data) { + prevStructureError = module.hot.data.prevError + } +} + +export function isSubscribable (thing) { + return thing && (typeof thing.then === 'function' || typeof thing.subscribe === 'function') +} + +export function isStructure (structure) { + return ( + structure && + (typeof structure === 'function' || + typeof structure.serialize !== 'function' || + typeof structure.then !== 'function' || + typeof structure.subscribe !== 'function' || + typeof structure.type !== 'string') + ) +} + +export function serializeStructure (item, context, resolverArgs = []) { + // Lazy + if (typeof item === 'function') { + return serializeStructure(item(...resolverArgs), context, resolverArgs) + } + + // Promise/observable returning a function, builder or plain JSON structure + if (isSubscribable(item)) { + return observableFrom(item).pipe( + mergeMap(val => serializeStructure(val, context, resolverArgs)) + ) + } + + // Builder? + if (item && typeof item.serialize === 'function') { + return serializeStructure(item.serialize(context)) + } + + // Plain value? + return observableOf(item) +} + +export function getDefaultStructure () { + const items = StructureBuilder.documentTypeListItems() + return StructureBuilder.list() + .id('__root__') + .title('Content') + .showIcons(items.some(item => item.getSchemaType().icon)) + .items(items) +} + +// We are lazy-requiring/resolving the structure inside of a function in order to catch errors +// on the root-level of the module. Any loading errors will be caught and emitted as errors +// eslint-disable-next-line complexity +export function loadStructure () { + let structure + try { + const mod = require('part:@sanity/desk-tool/structure?') || getDefaultStructure() + structure = mod && mod.__esModule ? mod.default : mod + + // On invalid modules, when HMR kicks in, we sometimes get an empty object back when the + // source has changed without fixing the problem. In this case, keep showing the error + if ( + __DEV__ && + prevStructureError && + structure && + structure.constructor.name === 'Object' && + Object.keys(structure).length === 0 + ) { + return throwError(prevStructureError) + } + + prevStructureError = null + } catch (err) { + prevStructureError = err + return throwError(err) + } + + if (!isStructure(structure)) { + return throwError( + new Error( + `Structure needs to export a function, an observable, a promise or a stucture builder, got ${typeof structure}` + ) + ) + } + + // Defer to catch immediately thrown errors on serialization + return defer(() => serializeStructure(structure)) +} diff --git a/studio/plugins/dashboard-widget-structure-menu/src/props.js b/studio/plugins/dashboard-widget-structure-menu/src/props.js new file mode 100644 index 0000000..65c725f --- /dev/null +++ b/studio/plugins/dashboard-widget-structure-menu/src/props.js @@ -0,0 +1,11 @@ +import {combineLatest} from 'rxjs' +import {map} from 'rxjs/operators' +import {loadStructure} from './lib/structure' + +export function toPropsStream (props$) { + const structure$ = loadStructure() + + return combineLatest(props$, structure$).pipe( + map(([props, structure]) => ({...props, structure})) + ) +} diff --git a/studio/plugins/dashboard-widget-structure-menu/src/widget.js b/studio/plugins/dashboard-widget-structure-menu/src/widget.js new file mode 100644 index 0000000..7cb18f8 --- /dev/null +++ b/studio/plugins/dashboard-widget-structure-menu/src/widget.js @@ -0,0 +1,10 @@ +import {withPropsStream} from 'react-props-stream' +import {withRouterHOC} from 'part:@sanity/base/router' +import {StructureMenuWidget} from './components' +import {toPropsStream} from './props' + +export default { + name: 'structure-menu', + component: withRouterHOC(withPropsStream(toPropsStream, StructureMenuWidget)), + layout: {width: 'full'} +} diff --git a/studio/sanity.json b/studio/sanity.json new file mode 100644 index 0000000..f3f74c6 --- /dev/null +++ b/studio/sanity.json @@ -0,0 +1,35 @@ +{ + "root": true, + "project": { + "name": "TestGatsbySanity" + }, + "api": { + "projectId": "dkwlsoid", + "dataset": "production" + }, + "plugins": [ + "@sanity/base", + "@sanity/components", + "@sanity/default-layout", + "@sanity/default-login", + "@sanity/dashboard", + "@sanity/desk-tool", + "dashboard-widget-structure-menu", + "dashboard-widget-document-list", + "dashboard-widget-netlify" + ], + "parts": [ + { + "name": "part:@sanity/base/schema", + "path": "./schemas/schema.js" + }, + { + "name": "part:@sanity/desk-tool/structure", + "path": "./deskStructure.js" + }, + { + "implements": "part:@sanity/dashboard/config", + "path": "./dashboardConfig.js" + } + ] +} \ No newline at end of file diff --git a/studio/schemas/documents/category.js b/studio/schemas/documents/category.js new file mode 100644 index 0000000..bf0f781 --- /dev/null +++ b/studio/schemas/documents/category.js @@ -0,0 +1,17 @@ +export default { + name: 'category', + type: 'document', + title: 'Category', + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'description', + type: 'text', + title: 'Description' + } + ] +} diff --git a/studio/schemas/documents/person.js b/studio/schemas/documents/person.js new file mode 100644 index 0000000..4a8beb1 --- /dev/null +++ b/studio/schemas/documents/person.js @@ -0,0 +1,41 @@ +import MdPerson from 'react-icons/lib/md/person' + +export default { + name: 'person', + type: 'document', + title: 'Person', + icon: MdPerson, + fields: [ + { + name: 'name', + type: 'string', + title: 'Name' + }, + { + name: 'slug', + type: 'slug', + title: 'Slug', + description: 'Some frontend will require a slug to be set to be able to show the person', + options: { + source: 'name', + maxLength: 96 + } + }, + { + name: 'image', + title: 'Image', + type: 'figure' + }, + { + name: 'bio', + title: 'Bio', + type: 'bioPortableText' + } + ], + preview: { + select: { + title: 'name', + media: 'image' + } + } +} diff --git a/studio/schemas/documents/project.js b/studio/schemas/documents/project.js new file mode 100644 index 0000000..23f1a68 --- /dev/null +++ b/studio/schemas/documents/project.js @@ -0,0 +1,90 @@ +import {format} from 'date-fns' + +export default { + name: 'project', + title: 'Project', + type: 'document', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'slug', + title: 'Slug', + type: 'slug', + description: 'Some frontend will require a slug to be set to be able to show the project', + options: { + source: 'title', + maxLength: 96 + } + }, + { + name: 'publishedAt', + title: 'Published at', + description: 'You can use this field to schedule projects where you show them', + type: 'datetime' + }, + { + name: 'excerpt', + title: 'Excerpt', + type: 'simplePortableText' + }, + { + name: 'members', + title: 'Members', + type: 'array', + of: [{type: 'projectMember'}] + }, + { + name: 'startedAt', + title: 'Started at', + type: 'datetime' + }, + { + name: 'endedAt', + title: 'Ended at', + type: 'datetime' + }, + { + name: 'mainImage', + title: 'Main image', + type: 'figure' + }, + { + name: 'categories', + title: 'Categories', + type: 'array', + of: [{type: 'reference', to: {type: 'category'}}] + }, + { + name: 'body', + title: 'Body', + type: 'projectPortableText' + }, + { + name: 'relatedProjects', + title: 'Related projects', + type: 'array', + of: [{type: 'reference', to: {type: 'project'}}] + } + ], + preview: { + select: { + title: 'title', + publishedAt: 'publishedAt', + slug: 'slug', + media: 'mainImage' + }, + prepare ({title = 'No title', publishedAt, slug, media}) { + const dateSegment = format(publishedAt, 'YYYY/MM') + const path = `/${dateSegment}/${slug.current}/` + return { + title, + media, + subtitle: publishedAt ? path : 'Missing publishing date' + } + } + } +} diff --git a/studio/schemas/documents/siteSettings.js b/studio/schemas/documents/siteSettings.js new file mode 100644 index 0000000..1cbec3c --- /dev/null +++ b/studio/schemas/documents/siteSettings.js @@ -0,0 +1,35 @@ +export default { + name: 'siteSettings', + type: 'document', + title: 'Site Settings', + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'description', + type: 'text', + title: 'Description', + description: 'Describe your portfolio for search engines and social media.' + }, + { + name: 'keywords', + type: 'array', + title: 'Keywords', + description: 'Add keywords that describes your portfolio.', + of: [{type: 'string'}], + options: { + layout: 'tags' + } + }, + { + name: 'author', + type: 'reference', + description: 'Publish an author and set a reference to them here.', + title: 'Author', + to: [{type: 'person'}] + } + ] +} diff --git a/studio/schemas/objects/bioPortableText.js b/studio/schemas/objects/bioPortableText.js new file mode 100644 index 0000000..25531db --- /dev/null +++ b/studio/schemas/objects/bioPortableText.js @@ -0,0 +1,34 @@ +export default { + name: 'bioPortableText', + type: 'array', + title: 'Excerpt', + of: [ + { + title: 'Block', + type: 'block', + styles: [{title: 'Normal', value: 'normal'}], + lists: [], + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Code', value: 'code'} + ], + annotations: [ + { + name: 'link', + type: 'object', + title: 'URL', + fields: [ + { + title: 'URL', + name: 'href', + type: 'url' + } + ] + } + ] + } + } + ] +} diff --git a/studio/schemas/objects/figure.js b/studio/schemas/objects/figure.js new file mode 100644 index 0000000..226f6db --- /dev/null +++ b/studio/schemas/objects/figure.js @@ -0,0 +1,34 @@ +export default { + name: 'figure', + title: 'Image', + type: 'image', + options: { + hotspot: true + }, + fields: [ + { + title: 'Caption', + name: 'caption', + type: 'string', + options: { + isHighlighted: true + } + }, + { + name: 'alt', + type: 'string', + title: 'Alternative text', + validation: Rule => Rule.error('You have to fill out the alternative text.').required(), + description: 'Important for SEO and accessiblity.', + options: { + isHighlighted: true + } + } + ], + preview: { + select: { + imageUrl: 'asset.url', + title: 'caption' + } + } +} diff --git a/studio/schemas/objects/projectMember.js b/studio/schemas/objects/projectMember.js new file mode 100644 index 0000000..226476a --- /dev/null +++ b/studio/schemas/objects/projectMember.js @@ -0,0 +1,42 @@ +export default { + type: 'object', + name: 'projectMember', + title: 'Project Member', + fields: [ + { + title: 'Person', + name: 'person', + type: 'reference', + to: {type: 'person'} + }, + { + title: 'Roles', + name: 'roles', + type: 'array', + of: [{type: 'string'}], + options: { + layout: 'radio', + list: [ + {title: 'Designer', value: 'designer'}, + {title: 'Developer', value: 'developer'}, + {title: 'Editor', value: 'editor'}, + {title: 'Manager', value: 'manager'} + ] + } + } + ], + preview: { + select: { + personName: 'person.name', + roles: 'roles', + media: 'person.image' + }, + prepare (data) { + return { + ...data, + title: data.personName, + subtitle: data.roles && data.roles.join('/') + } + } + } +} diff --git a/studio/schemas/objects/projectPortableText.js b/studio/schemas/objects/projectPortableText.js new file mode 100644 index 0000000..823c0a0 --- /dev/null +++ b/studio/schemas/objects/projectPortableText.js @@ -0,0 +1,51 @@ +export default { + title: 'Portable Text', + name: 'projectPortableText', + type: 'array', + of: [ + { + title: 'Block', + type: 'block', + // Styles let you set what your user can mark up blocks with. These + // corrensponds with HTML tags, but you can set any title or value + // you want and decide how you want to deal with it where you want to + // use your content. + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + {title: 'H3', value: 'h3'}, + {title: 'H4', value: 'h4'}, + {title: 'Quote', value: 'blockquote'} + ], + lists: [{title: 'Bullet', value: 'bullet'}], + // Marks let you mark up inline text in the block editor. + marks: { + // Decorators usually describe a single property – e.g. a typographic + // preference or highlighting by editors. + decorators: [{title: 'Strong', value: 'strong'}, {title: 'Emphasis', value: 'em'}], + // Annotations can be any object structure – e.g. a link or a footnote. + annotations: [ + { + title: 'URL', + name: 'link', + type: 'object', + fields: [ + { + title: 'URL', + name: 'href', + type: 'url' + } + ] + } + ] + } + }, + // You can add additional types here. Note that you can't use + // primitive types such as 'string' and 'number' in the same array + // as a block type. + { + type: 'figure' + } + ] +} diff --git a/studio/schemas/objects/simplePortableText.js b/studio/schemas/objects/simplePortableText.js new file mode 100644 index 0000000..e48ecf2 --- /dev/null +++ b/studio/schemas/objects/simplePortableText.js @@ -0,0 +1,39 @@ +/** + * This is the schema definition for the rich text fields used for + * for this blog studio. When you import it in schemas.js it can be + * reused in other parts of the studio with: + * { + * name: 'someName', + * title: 'Some title', + * type: 'simplePortableText' + * } + */ +export default { + title: 'Portable Text', + name: 'simplePortableText', + type: 'array', + of: [ + { + title: 'Block', + type: 'block', + // Styles let you set what your user can mark up blocks with. These + // corrensponds with HTML tags, but you can set any title or value + // you want and decide how you want to deal with it where you want to + // use your content. + styles: [{title: 'Normal', value: 'normal'}], + lists: [], + // Marks let you mark up inline text in the block editor. + marks: { + // Decorators usually describe a single property – e.g. a typographic + // preference or highlighting by editors. + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Code', value: 'code'} + ], + // Annotations can be any object structure – e.g. a link or a footnote. + annotations: [] + } + } + ] +} diff --git a/studio/schemas/schema.js b/studio/schemas/schema.js new file mode 100644 index 0000000..e8b08a2 --- /dev/null +++ b/studio/schemas/schema.js @@ -0,0 +1,41 @@ +// First, we must import the schema creator +import createSchema from 'part:@sanity/base/schema-creator' + +// Then import schema types from any plugins that might expose them +import schemaTypes from 'all:part:@sanity/base/schema-type' + +// Document types +import category from './documents/category' +import person from './documents/person' +import project from './documents/project' +import siteSettings from './documents/siteSettings' + +// Object types +import bioPortableText from './objects/bioPortableText' +import figure from './objects/figure' +import projectMember from './objects/projectMember' +import projectPortableText from './objects/projectPortableText' +import simplePortableText from './objects/simplePortableText' + +// Then we give our schema to the builder and provide the result to Sanity +export default createSchema({ + // We name our schema + name: 'portfolio', + // Then proceed to concatenate our our document type + // to the ones provided by any plugins that are installed + types: schemaTypes.concat([ + // When added to this list, object types can be used as + // { type: 'typename' } in other document schemas + bioPortableText, + figure, + projectMember, + projectPortableText, + simplePortableText, + // The following are document types which will appear + // in the studio. + category, + person, + project, + siteSettings + ]) +}) diff --git a/studio/static/.gitkeep b/studio/static/.gitkeep new file mode 100644 index 0000000..37178a7 --- /dev/null +++ b/studio/static/.gitkeep @@ -0,0 +1 @@ +Files placed here will be served by the Sanity server under the `/static`-prefix diff --git a/studio/static/favicon.ico b/studio/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7305cdbf5c90a28ce1786cd0f85e8cad9f37e5ba GIT binary patch literal 1150 zcma)*y-R{o6vj_c)DUS6K`k}eu-5cQA<#OdgnLGzB$wTd$c)i|fbVYR4R)9Zj)&s8x&#UD_cjfVTR;fqG zH$kq(JyRPrd%&GpUgDO?`+U9udSu|viKiw{eM#2?D}g|u+vIf)H-~N=|4TEzU5!2W z;oHTpXLHw1YjNm@@hza&J-^kwRq}mHV@C9Pj!86+mfjwzr^4ZIuj$h{ToioT^|t&T zE{2yI);QqCLB*!eb6zLZ7X5zzNTZ{^f-bIJVuoj!O(YVX^@{d0yvN`M?|d_*_Jf;H za2NFRs2lp;Svv~!`KRhgCe&lPKLz==$#Z8Ma7Q)%otR{;2z{5yFTi(=E4Dfh^fUDS zd(WyjP(m|lu5F)dZ`M4Z&x7^?1&{)9{RZq>&A&!PN !isFuture(edge.node.publishedAt)) + .forEach(edge => { + const id = edge.node.id + const slug = edge.node.slug.current + const path = `/project/${slug}/` + + reporter.info(`Creating project page: ${path}`) + + createPage({ + path, + component: require.resolve('./src/templates/project.js'), + context: {id} + }) + + createPageDependency({path, nodeId: id}) + }) +} + +exports.createPages = async ({graphql, actions, reporter}) => { + await createProjectPages(graphql, actions, reporter) +} diff --git a/web/gatsby-ssr.js b/web/gatsby-ssr.js new file mode 100644 index 0000000..b17b8fc --- /dev/null +++ b/web/gatsby-ssr.js @@ -0,0 +1,7 @@ +/** + * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. + * + * See: https://www.gatsbyjs.org/docs/ssr-apis/ + */ + +// You can delete this file if you're not using it diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..37ffda2 --- /dev/null +++ b/web/package.json @@ -0,0 +1,42 @@ +{ + "private": true, + "name": "TestGatsbySanity-web", + "version": "1.0.0", + "author": "Sanity ", + "scripts": { + "build": "gatsby build", + "clean-cache": "gatsby clean", + "dev": "npm run clean-cache && gatsby develop", + "lint": "eslint .", + "format": "prettier-eslint --write \"**/*.js\" \"!.cache/**\" \"!node_modules/**\" \"!public/**\"", + "test": "echo \"Write tests! -> https://gatsby.app/unit-testing\"" + }, + "devDependencies": { + "@sanity/block-content-to-react": "^2.0.6", + "@sanity/image-url": "^0.140.12", + "date-fns": "^1.30.1", + "dotenv": "^8.0.0", + "eslint": "^5.16.0", + "eslint-config-standard": "^12.0.0", + "eslint-config-standard-react": "^7.0.2", + "eslint-plugin-import": "^2.17.2", + "eslint-plugin-node": "^9.0.1", + "eslint-plugin-promise": "^4.1.1", + "eslint-plugin-react": "^7.13.0", + "eslint-plugin-standard": "^4.0.0", + "gatsby": "^2.4.2", + "gatsby-plugin-postcss": "^2.0.7", + "gatsby-plugin-react-helmet": "^3.0.12", + "gatsby-source-sanity": "^4.0.1", + "postcss-import": "^12.0.1", + "postcss-preset-env": "^6.6.0", + "prettier-eslint-cli": "^4.7.1", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-helmet": "^5.2.0", + "rimraf": "^2.6.3" + }, + "dependencies": { + "gatsby-image": "^2.0.41" + } +} \ No newline at end of file diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..74ba2b6 --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,8 @@ +module.exports = () => ({ + plugins: [ + require('postcss-import'), + require('postcss-preset-env')({ + stage: 0 + }) + ] +}) diff --git a/web/src/components/block-content.js b/web/src/components/block-content.js new file mode 100644 index 0000000..ac15370 --- /dev/null +++ b/web/src/components/block-content.js @@ -0,0 +1,10 @@ +import BaseBlockContent from '@sanity/block-content-to-react' +import React from 'react' +import clientConfig from '../../client-config' +import serializers from './serializers' + +const BlockContent = ({blocks}) => ( + +) + +export default BlockContent diff --git a/web/src/components/block-text.js b/web/src/components/block-text.js new file mode 100644 index 0000000..fb34a3b --- /dev/null +++ b/web/src/components/block-text.js @@ -0,0 +1,7 @@ +import BaseBlockContent from '@sanity/block-content-to-react' +import React from 'react' +import serializers from './serializers' + +const BlockText = ({blocks}) => + +export default BlockText diff --git a/web/src/components/container.js b/web/src/components/container.js new file mode 100644 index 0000000..58e6a29 --- /dev/null +++ b/web/src/components/container.js @@ -0,0 +1,9 @@ +import React from 'react' + +import styles from './container.module.css' + +const Container = ({children}) => { + return
{children}
+} + +export default Container diff --git a/web/src/components/container.module.css b/web/src/components/container.module.css new file mode 100644 index 0000000..65de03d --- /dev/null +++ b/web/src/components/container.module.css @@ -0,0 +1,12 @@ +@import '../styles/custom-media.css'; + +.root { + box-sizing: border-box; + max-width: 960px; + padding: 1.5em; + margin: 0 auto; + + @media (--media-min-small) { + padding: 2em; + } +} diff --git a/web/src/components/figure.js b/web/src/components/figure.js new file mode 100644 index 0000000..3b7c862 --- /dev/null +++ b/web/src/components/figure.js @@ -0,0 +1,21 @@ +import React from 'react' +import Img from 'gatsby-image' +import {getFluidGatsbyImage} from 'gatsby-source-sanity' +import clientConfig from '../../client-config' + +import styles from './figure.module.css' + +export default ({node}) => { + if (!node.asset) { + return null + } + + const fluidProps = getFluidGatsbyImage(node.asset._ref, {maxWidth: 675}, ...clientConfig.sanity) + + return ( +
+ {node.alt} + {node.caption &&
{node.caption}
} +
+ ) +} diff --git a/web/src/components/figure.module.css b/web/src/components/figure.module.css new file mode 100644 index 0000000..27f09d7 --- /dev/null +++ b/web/src/components/figure.module.css @@ -0,0 +1,11 @@ +@import '../styles/custom-properties.css'; + +.root { + margin: 2rem 0; + + @nest & figcaption { + font-size: var(--font-small-size); + line-height: var(--font-small-line-height); + margin: 0.5rem 0 0; + } +} diff --git a/web/src/components/graphql-error-list.js b/web/src/components/graphql-error-list.js new file mode 100644 index 0000000..55e145e --- /dev/null +++ b/web/src/components/graphql-error-list.js @@ -0,0 +1,12 @@ +import React from 'react' + +const GraphQLErrorList = ({errors}) => ( +
+

GraphQL Error

+ {errors.map(error => ( +
{error.message}
+ ))} +
+) + +export default GraphQLErrorList diff --git a/web/src/components/header.js b/web/src/components/header.js new file mode 100644 index 0000000..3726260 --- /dev/null +++ b/web/src/components/header.js @@ -0,0 +1,30 @@ +import {Link} from 'gatsby' +import React from 'react' +import Icon from './icon' +import {cn} from '../lib/helpers' + +import styles from './header.module.css' + +const Header = ({onHideNav, onShowNav, showNav, siteTitle}) => ( +
+
+
+ {siteTitle} +
+ + + + +
+
+) + +export default Header diff --git a/web/src/components/header.module.css b/web/src/components/header.module.css new file mode 100644 index 0000000..7477411 --- /dev/null +++ b/web/src/components/header.module.css @@ -0,0 +1,114 @@ +@import '../styles/custom-media.css'; +@import '../styles/custom-properties.css'; + +.root { + position: relative; + z-index: 100; +} + +.wrapper { + box-sizing: border-box; + margin: 0 auto; + max-width: 960px; + padding: 1em; + display: flex; + + @media (--media-min-small) { + padding: 1.5em 1.5em; + } +} + +.branding { + font-weight: 600; + flex: 1; + + @nest & a { + display: inline-block; + padding: 0.5em; + color: inherit; + text-decoration: none; + + @media (hover: hover) { + @nest &:hover { + color: var(--color-accent); + } + } + } +} + +.toggleNavButton { + appearance: none; + font-size: 25px; + border: none; + background: none; + margin: 0; + padding: calc(14 / 17 / 2 * 1rem); + outline: none; + color: inherit; + + & svg { + display: block; + fill: inherit; + } + + @media (--media-min-small) { + display: none; + } +} + +.nav { + display: none; + + @nest & ul { + margin: 0; + padding: 0; + } + + @nest & ul li a { + display: block; + color: inherit; + text-decoration: none; + } + + @media (hover: hover) { + @nest & ul li a:hover { + color: var(--color-accent); + } + } + + @media (--media-max-small) { + position: absolute; + background: var(--color-white); + color: var(--color-black); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.25); + left: 0; + right: 0; + top: 4.3rem; + + @nest & ul { + padding: 1rem 0; + } + + @nest & ul li a { + padding: 0.5rem 1.5rem; + } + } + + @media (--media-min-small) { + display: block; + + @nest & ul { + list-style: none; + display: flex; + justify-content: flex-end; + } + + @nest & ul li a { + padding: 0.5rem; + } + } +} + +.showNav { + display: block; +} diff --git a/web/src/components/icon/hamburger.js b/web/src/components/icon/hamburger.js new file mode 100644 index 0000000..25bdf72 --- /dev/null +++ b/web/src/components/icon/hamburger.js @@ -0,0 +1,20 @@ +import React from 'react' + +const strokeStyle = {vectorEffect: 'non-scaling-stroke'} + +const HamburgerIcon = () => ( + + + + + +) + +export default HamburgerIcon diff --git a/web/src/components/icon/index.js b/web/src/components/icon/index.js new file mode 100644 index 0000000..14a41ae --- /dev/null +++ b/web/src/components/icon/index.js @@ -0,0 +1,13 @@ +import React from 'react' +import HamburgerIcon from './hamburger' + +function Icon (props) { + switch (props.symbol) { + case 'hamburger': + return + default: + return Unknown icon: {props.symbol} + } +} + +export default Icon diff --git a/web/src/components/layout.js b/web/src/components/layout.js new file mode 100644 index 0000000..3375faf --- /dev/null +++ b/web/src/components/layout.js @@ -0,0 +1,23 @@ +import React from 'react' +import Header from './header' + +import '../styles/layout.css' +import styles from './layout.module.css' + +const Layout = ({children, onHideNav, onShowNav, showNav, siteTitle}) => ( + <> +
+
{children}
+
+
+
+ © {new Date().getFullYear()}, Built with Sanity & + {` `} + Gatsby +
+
+
+ +) + +export default Layout diff --git a/web/src/components/layout.module.css b/web/src/components/layout.module.css new file mode 100644 index 0000000..985d71f --- /dev/null +++ b/web/src/components/layout.module.css @@ -0,0 +1,48 @@ +@import '../styles/custom-media.css'; +@import '../styles/custom-properties.css'; + +.content { + background: var(--color-white); + min-height: calc(100% - 73px - 120px); + + @media (--media-min-small) { + min-height: calc(100% - 88px - 150px); + } +} + +.footer { + border-top: 1px solid var(--color-very-light-gray); + + @nest & a { + color: inherit; + text-decoration: none; + + @media (hover: hover) { + @nest &:hover { + color: var(--color-accent); + } + } + } +} + +.footerWrapper { + box-sizing: border-box; + max-width: 960px; + padding: 4.5em 1.5em 1.5em; + margin: 0 auto; + + @media (--media-min-small) { + padding: 6em 2em 2em; + } +} + +.companyAddress { + text-align: center; + margin: 0 0 1rem; +} + +.siteInfo { + text-align: center; + font-size: var(--font-small-size); + line-height: var(--font-small-line-height); +} diff --git a/web/src/components/project-preview-grid.js b/web/src/components/project-preview-grid.js new file mode 100644 index 0000000..74b138b --- /dev/null +++ b/web/src/components/project-preview-grid.js @@ -0,0 +1,34 @@ +import {Link} from 'gatsby' +import React from 'react' +import ProjectPreview from './project-preview' + +import styles from './project-preview-grid.module.css' + +function ProjectPreviewGrid (props) { + return ( +
+ {props.title &&

{props.title}

} +
    + {props.nodes && + props.nodes.map(node => ( +
  • + +
  • + ))} +
+ {props.browseMoreHref && ( +
+ Browse more +
+ )} +
+ ) +} + +ProjectPreviewGrid.defaultProps = { + title: '', + nodes: [], + browseMoreHref: '' +} + +export default ProjectPreviewGrid diff --git a/web/src/components/project-preview-grid.module.css b/web/src/components/project-preview-grid.module.css new file mode 100644 index 0000000..b7b7999 --- /dev/null +++ b/web/src/components/project-preview-grid.module.css @@ -0,0 +1,51 @@ +@import '../styles/custom-media.css'; +@import '../styles/custom-properties.css'; + +.root { + margin: 2em 0 4em; +} + +.headline { + font-size: var(--font-micro-size); + line-height: var(--font-micro-line-height); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin: 2rem 0; +} + +.grid { + margin: 0; + padding: 0; + list-style: none; + display: grid; + grid-template-columns: 1fr; + grid-gap: 2em; + + @media (--media-min-small) { + grid-template-columns: 1fr 1fr; + } + + @media (--media-min-medium) { + grid-template-columns: 1fr 1fr 1fr; + } +} + +.browseMoreNav { + composes: small from './typography.module.css'; + margin-top: 1rem; + text-align: right; + + @nest & a { + display: inline-block; + padding: 0.5rem 0; + color: inherit; + text-decoration: none; + + @media (hover: hover) { + @nest &:hover { + color: var(--color-accent); + } + } + } +} diff --git a/web/src/components/project-preview.js b/web/src/components/project-preview.js new file mode 100644 index 0000000..a48bc76 --- /dev/null +++ b/web/src/components/project-preview.js @@ -0,0 +1,34 @@ +import {Link} from 'gatsby' +import React from 'react' +import {cn, buildImageObj} from '../lib/helpers' +import {imageUrlFor} from '../lib/image-url' +import BlockText from './block-text' + +import styles from './project-preview.module.css' +import {responsiveTitle3} from './typography.module.css' + +function ProjectPreview (props) { + return ( + +
+ {props.mainImage && props.mainImage.asset && ( + {props.mainImage.alt} + )} +
+

{props.title}

+ {props._rawExcerpt && ( +
+ +
+ )} + + ) +} + +export default ProjectPreview diff --git a/web/src/components/project-preview.module.css b/web/src/components/project-preview.module.css new file mode 100644 index 0000000..8e4b0bf --- /dev/null +++ b/web/src/components/project-preview.module.css @@ -0,0 +1,42 @@ +.root { + display: block; + color: inherit; + text-decoration: none; +} + +.title { + composes: responsiveTitle1 from './typography.module.css'; +} + +.leadMediaThumb { + position: relative; + padding-bottom: 66.666%; + background: #eee; + + @nest & img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.title { + @media (hover: hover) { + @nest .root:hover & { + text-decoration: underline; + } + } +} + +.excerpt { + @nest & p { + margin: 0.5em 0; + } + + @nest & strong { + font-weight: 600; + } +} diff --git a/web/src/components/project.js b/web/src/components/project.js new file mode 100644 index 0000000..132f690 --- /dev/null +++ b/web/src/components/project.js @@ -0,0 +1,76 @@ +import {format, distanceInWords, differenceInDays} from 'date-fns' +import React from 'react' +import {Link} from 'gatsby' +import {buildImageObj} from '../lib/helpers' +import {imageUrlFor} from '../lib/image-url' +import BlockContent from './block-content' +import Container from './container' +import RoleList from './role-list' + +import styles from './project.module.css' + +function Project (props) { + const {_rawBody, title, categories, mainImage, members, publishedAt, relatedProjects} = props + return ( +
+ {props.mainImage && mainImage.asset && ( +
+ {mainImage.alt} +
+ )} + +
+
+

{title}

+ {_rawBody && } +
+ +
+
+
+ ) +} + +export default Project diff --git a/web/src/components/project.module.css b/web/src/components/project.module.css new file mode 100644 index 0000000..580533f --- /dev/null +++ b/web/src/components/project.module.css @@ -0,0 +1,98 @@ +@import '../styles/custom-media.css'; +@import '../styles/custom-properties.css'; + +.root {} + +.title { + composes: responsiveTitle1 from './typography.module.css'; +} + +.mainImage { + position: relative; + background: #eee; + padding-bottom: calc(9 / 16 * 100%); + + @nest & img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + vertical-align: top; + object-fit: cover; + } +} + +.grid { + display: grid; + grid-template-columns: 1fr; + grid-column-gap: 2em; + + @media (--media-min-medium) { + grid-template-columns: 3fr 1fr; + } +} + +.mainContent { + @nest & a { + color: var(--color-accent); + + @media (hover: hover) { + @nest &:hover { + color: inherit; + } + } + } +} + +.metaContent { +} + +.publishedAt { + composes: small from './typography.module.css'; + margin: 1.5rem 0 3rem; + color: var(--color-gray); +} + +.categories { + border-top: 1px solid var(--color-very-light-gray); + margin: 2rem 0 3rem; + + @nest & ul { + list-style: none; + margin: 0.75rem 0; + padding: 0; + } + + @nest & ul li { + padding: 0.25rem 0; + } +} + +.categoriesHeadline { + composes: base from './typography.module.css'; + margin: 0.5rem 0 0; +} + +.relatedProjects { + border-top: 1px solid var(--color-very-light-gray); + margin: 2rem 0 3rem; + + @nest & ul { + list-style: none; + margin: 0.75rem 0; + padding: 0; + } + + @nest & a { + display: inline-block; + color: inherit; + text-decoration: none; + padding: 0.25rem 0; + } +} + +.relatedProjectsHeadline { + composes: base from './typography.module.css'; + margin: 0.5rem 0 0; +} diff --git a/web/src/components/role-list.js b/web/src/components/role-list.js new file mode 100644 index 0000000..a9c6095 --- /dev/null +++ b/web/src/components/role-list.js @@ -0,0 +1,55 @@ +import React from 'react' +import {buildImageObj} from '../lib/helpers' +import {imageUrlFor} from '../lib/image-url' +import {ucfirst} from '../lib/string-utils' + +import styles from './role-list.module.css' + +function RoleList ({items, title}) { + return ( +
+

{title}

+
    + {items.map(item => ( +
  • +
    +
    + {item.person && item.person.image && item.person.image.asset && ( + + )} +
    +
    +
    +
    + {(item.person && item.person.name) || Missing name} +
    + {item.roles && ( +
    + {item.roles.map((role, idx) => { + switch (true) { + case idx === 0: + return {ucfirst(role)} + case idx === item.roles.length - 1: + return & {role} + default: + return , {role} + } + })} +
    + )} +
    +
  • + ))} +
+
+ ) +} + +export default RoleList diff --git a/web/src/components/role-list.module.css b/web/src/components/role-list.module.css new file mode 100644 index 0000000..cc0e293 --- /dev/null +++ b/web/src/components/role-list.module.css @@ -0,0 +1,46 @@ +@import '../styles/custom-properties.css'; + +.root { + margin: 2rem 0 3rem; + border-top: 1px solid var(--color-very-light-gray); +} + +.headline { + composes: base from './typography.module.css'; + margin: 0.5rem 0 0; +} + +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.listItem { + font-size: var(--font-small-size); + margin: 1rem 0; + display: flex; + justify-content: center; + align-items: center; + + @nest & > div:last-child { + flex: 1; + margin-left: 0.75rem; + } +} + +.avatar { + position: relative; + width: 3em; + height: 3em; + background: #eee; + border-radius: 50%; + overflow: hidden; + + @nest & img { + width: 100%; + height: 100%; + vertical-align: top; + object-fit: cover; + } +} diff --git a/web/src/components/seo.js b/web/src/components/seo.js new file mode 100644 index 0000000..1cad0a2 --- /dev/null +++ b/web/src/components/seo.js @@ -0,0 +1,96 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Helmet from 'react-helmet' +import {StaticQuery, graphql} from 'gatsby' + +function SEO ({description, lang, meta, keywords, title}) { + return ( + { + const metaDescription = description || (data.site && data.site.description) || '' + const siteTitle = (data.site && data.site.title) || '' + const siteAuthor = (data.site && data.site.author && data.site.author.name) || '' + return ( + 0 + ? { + name: 'keywords', + content: keywords.join(', ') + } + : [] + ) + .concat(meta)} + /> + ) + }} + /> + ) +} + +SEO.defaultProps = { + lang: 'en', + meta: [], + keywords: [] +} + +SEO.propTypes = { + description: PropTypes.string, + lang: PropTypes.string, + meta: PropTypes.array, + keywords: PropTypes.arrayOf(PropTypes.string), + title: PropTypes.string.isRequired +} + +export default SEO + +const detailsQuery = graphql` + query DefaultSEOQuery { + site: sanitySiteSettings(_id: {eq: "siteSettings"}) { + title + description + keywords + author { + name + } + } + } +` diff --git a/web/src/components/serializers.js b/web/src/components/serializers.js new file mode 100644 index 0000000..526def2 --- /dev/null +++ b/web/src/components/serializers.js @@ -0,0 +1,9 @@ +import Figure from './figure' + +const serializers = { + types: { + figure: Figure + } +} + +export default serializers diff --git a/web/src/components/typography.module.css b/web/src/components/typography.module.css new file mode 100644 index 0000000..774448a --- /dev/null +++ b/web/src/components/typography.module.css @@ -0,0 +1,156 @@ +@import '../styles/custom-media.css'; + +:root { + --base-unit: 17; + + /* Font sizes */ + --font-micro-size: calc(8 / var(--base-unit) * 1rem); + --font-micro-line-height: 1; + --font-small-size: calc(14 / var(--base-unit) * 1rem); + --font-small-line-height: calc(18 / 14); + --font-base-size: calc(var(--base-unit) / 16 * 100%); + --font-base-line-height: calc(22 / var(--base-unit)); + --font-large-size: calc(19 / var(--base-unit) * 1rem); + --font-large-line-height: calc(24 / 19); + --font-title3-size: calc(24 / var(--base-unit) * 1rem); + --font-title3-line-height: calc(28 / 24); + --font-title2-size: calc(32 / var(--base-unit) * 1rem); + --font-title2-line-height: calc(36 / 32); + --font-title1-size: calc(44 / var(--base-unit) * 1rem); + --font-title1-line-height: calc(56 / 44); +} + + + +/* + * Statically sized elements + */ + +.title1 { + font-size: var(--font-title1-size); + line-height: var(--font-title1-line-height); +} + +.title2 { + font-size: var(--font-title2-size); + line-height: var(--font-title2-line-height); +} + +.title3 { + font-size: var(--font-title3-size); + line-height: var(--font-title3-line-height); +} + +.large { + font-size: var(--font-large-size); + line-height: var(--font-large-line-height); +} + +.base { + font-size: inherit; + line-height: inherit; +} + +.small { + font-size: var(--font-small-size); + line-height: var(--font-small-line-height); +} + +.micro { + font-size: var(--font-micro-size); + line-height: var(--font-micro-line-height); + text-transform: uppercase; +} + + + +/* + * Responsively sized elements + */ + +.paragraph { + font-size: var(--font-base-size); + line-height: var(--font-base-line-height); + margin: 0.5rem 0 1rem 0; + + @media (--media-min-small) { + font-size: var(--font-base-size); + line-height: var(--font-base-line-height); + } + + @media (--media-min-medium) { + font-size: var(--font-large-size); + line-height: var(--font-large-line-height); + } +} + +.blockQuote { + background: #eee; +} + +.responsiveTitle1 { + font-weight: 900; + font-size: var(--font-title3-size); + line-height: var(--font-title3-line-height); + margin: 1rem 0 2rem; + + @media (--media-min-small) { + font-size: var(--font-title2-size); + line-height: var(--font-title2-line-height); + } + + @media (--media-min-medium) { + font-size: var(--font-title1-size); + line-height: var(--font-title1-line-height); + } +} + +.responsiveTitle2 { + font-weight: 900; + font-size: var(--font-large-size); + line-height: var(--font-large-line-height); + margin: 1.5rem 0 0.5rem; + + @media (--media-min-small) { + font-size: var(--font-title3-size); + line-height: var(--font-title3-line-height); + } + + @media (--media-min-medium) { + font-size: var(--font-title2-size); + line-height: var(--font-title2-line-height); + } +} + +.responsiveTitle3 { + font-weight: 900; + font-size: var(--font-large-size); + line-height: var(--font-large-line-height); + margin: 1rem 0 0.5rem; + + @media (--media-min-small) { + font-size: var(--font-large-size); + line-height: var(--font-large-line-height); + } + + @media (--media-min-medium) { + font-size: var(--font-title3-size); + line-height: var(--font-title3-line-height); + } +} + +.responsiveTitle4 { + font-size: var(--font-base-size); + line-height: var(--font-base-line-height); + margin: 1rem 0 0.5rem; + + @media (--media-min-small) { + font-size: var(--font-base-size); + line-height: var(--font-base-line-height); + } + + @media (--media-min-medium) { + font-size: var(--font-large-size); + line-height: var(--font-large-line-height); + } +} diff --git a/web/src/containers/layout.js b/web/src/containers/layout.js new file mode 100644 index 0000000..8da0244 --- /dev/null +++ b/web/src/containers/layout.js @@ -0,0 +1,44 @@ +import {graphql, StaticQuery} from 'gatsby' +import React, {useState} from 'react' +import Layout from '../components/layout' + +const query = graphql` + query SiteTitleQuery { + site: sanitySiteSettings(_id: {regex: "/(drafts.|)siteSettings/"}) { + title + } + } +` + +function LayoutContainer (props) { + const [showNav, setShowNav] = useState(false) + function handleShowNav () { + setShowNav(true) + } + function handleHideNav () { + setShowNav(false) + } + return ( + { + if (!data.site) { + throw new Error( + 'Missing "Site settings". Open the studio at http://localhost:3333 and add "Site settings" data' + ) + } + return ( + + ) + }} + /> + ) +} + +export default LayoutContainer diff --git a/web/src/lib/helpers.js b/web/src/lib/helpers.js new file mode 100644 index 0000000..59c4036 --- /dev/null +++ b/web/src/lib/helpers.js @@ -0,0 +1,33 @@ +import {format, isFuture} from 'date-fns' + +export function cn (...args) { + return args.filter(Boolean).join(' ') +} + +export function mapEdgesToNodes (data) { + if (!data.edges) return [] + return data.edges.map(edge => edge.node) +} + +export function filterOutDocsWithoutSlugs ({slug}) { + return (slug || {}).current +} + +export function filterOutDocsPublishedInTheFuture ({publishedAt}) { + return !isFuture(publishedAt) +} + +export function getBlogUrl (publishedAt, slug) { + return `/blog/${format(publishedAt, 'YYYY/MM')}/${slug.current || slug}/` +} + +export function buildImageObj (source) { + const imageObj = { + asset: {_ref: source.asset._ref || source.asset._id} + } + + if (source.crop) imageObj.crop = source.crop + if (source.hotspot) imageObj.hotspot = source.hotspot + + return imageObj +} diff --git a/web/src/lib/image-url.js b/web/src/lib/image-url.js new file mode 100644 index 0000000..84730b2 --- /dev/null +++ b/web/src/lib/image-url.js @@ -0,0 +1,8 @@ +import clientConfig from '../../client-config' +import imageUrlBuilder from '@sanity/image-url' + +const builder = imageUrlBuilder(clientConfig.sanity) + +export function imageUrlFor (source) { + return builder.image(source) +} diff --git a/web/src/lib/string-utils.js b/web/src/lib/string-utils.js new file mode 100644 index 0000000..c9f8efb --- /dev/null +++ b/web/src/lib/string-utils.js @@ -0,0 +1,3 @@ +export function ucfirst (str) { + return `${str.substr(0, 1).toUpperCase()}${str.substr(1)}` +} diff --git a/web/src/pages/404.js b/web/src/pages/404.js new file mode 100644 index 0000000..87910f8 --- /dev/null +++ b/web/src/pages/404.js @@ -0,0 +1,14 @@ +import React from 'react' + +import Layout from '../components/layout' +import SEO from '../components/seo' + +const NotFoundPage = () => ( + + +

NOT FOUND

+

You just hit a route that doesn't exist... the sadness.

+
+) + +export default NotFoundPage diff --git a/web/src/pages/archive.js b/web/src/pages/archive.js new file mode 100644 index 0000000..9a274b9 --- /dev/null +++ b/web/src/pages/archive.js @@ -0,0 +1,61 @@ +import React from 'react' +import {graphql} from 'gatsby' +import Container from '../components/container' +import GraphQLErrorList from '../components/graphql-error-list' +import ProjectPreviewGrid from '../components/project-preview-grid' +import SEO from '../components/seo' +import Layout from '../containers/layout' +import {mapEdgesToNodes, filterOutDocsWithoutSlugs} from '../lib/helpers' + +import {responsiveTitle1} from '../components/typography.module.css' + +export const query = graphql` + query ArchivePageQuery { + projects: allSanityProject( + limit: 12 + sort: {fields: [publishedAt], order: DESC} + filter: {slug: {current: {ne: null}}, publishedAt: {ne: null}} + ) { + edges { + node { + id + mainImage { + asset { + _id + } + alt + } + title + _rawExcerpt + slug { + current + } + } + } + } + } +` + +const ArchivePage = props => { + const {data, errors} = props + if (errors) { + return ( + + + + ) + } + const projectNodes = + data && data.projects && mapEdgesToNodes(data.projects).filter(filterOutDocsWithoutSlugs) + return ( + + + +

Projects

+ {projectNodes && projectNodes.length > 0 && } +
+
+ ) +} + +export default ArchivePage diff --git a/web/src/pages/index.js b/web/src/pages/index.js new file mode 100644 index 0000000..eb6d72d --- /dev/null +++ b/web/src/pages/index.js @@ -0,0 +1,103 @@ +import React from 'react' +import {graphql} from 'gatsby' +import { + mapEdgesToNodes, + filterOutDocsWithoutSlugs, + filterOutDocsPublishedInTheFuture +} from '../lib/helpers' +import Container from '../components/container' +import GraphQLErrorList from '../components/graphql-error-list' +import ProjectPreviewGrid from '../components/project-preview-grid' +import SEO from '../components/seo' +import Layout from '../containers/layout' + +export const query = graphql` + query IndexPageQuery { + site: sanitySiteSettings(_id: {regex: "/(drafts.|)siteSettings/"}) { + title + description + keywords + } + projects: allSanityProject( + limit: 6 + sort: {fields: [publishedAt], order: DESC} + filter: {slug: {current: {ne: null}}, publishedAt: {ne: null}} + ) { + edges { + node { + id + mainImage { + crop { + _key + _type + top + bottom + left + right + } + hotspot { + _key + _type + x + y + height + width + } + asset { + _id + } + alt + } + title + _rawExcerpt + slug { + current + } + } + } + } + } +` + +const IndexPage = props => { + const {data, errors} = props + + if (errors) { + return ( + + + + ) + } + + const site = (data || {}).site + const projectNodes = (data || {}).projects + ? mapEdgesToNodes(data.projects) + .filter(filterOutDocsWithoutSlugs) + .filter(filterOutDocsPublishedInTheFuture) + : [] + + if (!site) { + throw new Error( + 'Missing "Site settings". Open the studio at http://localhost:3333 and add some content to "Site settings" and restart the development server.' + ) + } + + return ( + + + +

Welcome to {site.title}

+ {projectNodes && ( + + )} +
+
+ ) +} + +export default IndexPage diff --git a/web/src/styles/custom-media.css b/web/src/styles/custom-media.css new file mode 100644 index 0000000..42b2b9c --- /dev/null +++ b/web/src/styles/custom-media.css @@ -0,0 +1,4 @@ +@custom-media --media-min-small (min-width: 450px); +@custom-media --media-max-small (max-width: 449px); +@custom-media --media-min-medium (min-width: 675px); +@custom-media --media-min-large (min-width: 900px); diff --git a/web/src/styles/custom-properties.css b/web/src/styles/custom-properties.css new file mode 100644 index 0000000..e3c40b4 --- /dev/null +++ b/web/src/styles/custom-properties.css @@ -0,0 +1,29 @@ +:root { + --font-family-sans: -apple-system, BlinkMacSystemFont, sans-serif; + + --color-black: #202123; + --color-dark-gray: #32373e; + --color-gray: #697a90; + --color-light-gray: #b4bcc7; + --color-very-light-gray: #e7ebed; + --color-white: #fff; + --color-accent: #156dff; + + /* Typography */ + --unit: 16; + --font-micro-size: calc(10 / var(--unit) * 1rem); /* 10px */ + --font-micro-line-height: calc(12 / 10); /* 12px */ + --font-small-size: calc(14 / var(--unit) * 1rem); /* 14px */ + --font-small-line-height: calc(21 / 14); /* 21px */ + --font-base-size: 1em; /* 16px */ + --font-base-line-height: calc(24 / var(--unit)); /* 24px */ + --font-large-size: calc(18 / var(--unit) * 1rem); /* 18px */ + --font-large-line-height: calc(27 / 18); /* 27px */ + + --font-title3-size: calc(21 / var(--unit) * 1rem); /* 21px */ + --font-title3-line-height: calc(30 / 21); /* 30px */ + --font-title2-size: calc(24 / var(--unit) * 1rem); /* 24px */ + --font-title2-line-height: calc(33 / 24); /* 33px */ + --font-title1-size: calc(49 / var(--unit) * 1rem); /* 49px */ + --font-title1-line-height: calc(57 / 49); /* 57px */ +} diff --git a/web/src/styles/layout.css b/web/src/styles/layout.css new file mode 100644 index 0000000..c259861 --- /dev/null +++ b/web/src/styles/layout.css @@ -0,0 +1,21 @@ +@import './custom-properties.css'; + +html { + font-family: var(--font-family-sans); + font-size: var(--font-base-size); + line-height: var(--font-base-line-height); +} + +body { + -webkit-font-smoothing: antialiased; + background: var(--color-white); + color: var(--color-black); + margin: 0; +} + +html, +body, +body > div, +body > div > div { + height: 100%; +} diff --git a/web/src/templates/project.js b/web/src/templates/project.js new file mode 100644 index 0000000..f1122dd --- /dev/null +++ b/web/src/templates/project.js @@ -0,0 +1,102 @@ +import React from 'react' +import {graphql} from 'gatsby' +import Container from '../components/container' +import GraphQLErrorList from '../components/graphql-error-list' +import Project from '../components/project' +import SEO from '../components/seo' +import Layout from '../containers/layout' + +export const query = graphql` + query ProjectTemplateQuery($id: String!) { + project: sanityProject(id: {eq: $id}) { + id + publishedAt + categories { + _id + title + } + relatedProjects { + title + _id + slug { + current + } + } + mainImage { + crop { + _key + _type + top + bottom + left + right + } + hotspot { + _key + _type + x + y + height + width + } + asset { + _id + } + alt + } + title + slug { + current + } + _rawBody + members { + _key + person { + image { + crop { + _key + _type + top + bottom + left + right + } + hotspot { + _key + _type + x + y + height + width + } + asset { + _id + } + } + name + } + roles + } + } + } +` + +const ProjectTemplate = props => { + const {data, errors} = props + const project = data && data.project + return ( + + {errors && } + {project && } + + {errors && ( + + + + )} + {project && } + + ) +} + +export default ProjectTemplate