Initial commit
This commit is contained in:
commit
5a3bf52ebb
86 changed files with 2639 additions and 0 deletions
8
.editorconfig
Normal file
8
.editorconfig
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Mac files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# lerna files
|
||||||
|
/lerna-debug.log
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
/package-lock.json
|
||||||
|
/yarn.lock
|
||||||
38
README.md
Normal file
38
README.md
Normal file
|
|
@ -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
|
||||||
|
|
||||||
|
[](https://slack.sanity.io/)
|
||||||
|
|
||||||
|
Join [Sanity’s developer community](https://slack.sanity.io) or ping us [on twitter](https://twitter.com/sanity_io).
|
||||||
7
lerna.json
Normal file
7
lerna.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
"web",
|
||||||
|
"studio"
|
||||||
|
],
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
studio/.eslintignore
Normal file
1
studio/.eslintignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
dist/*
|
||||||
16
studio/.eslintrc.js
Normal file
16
studio/.eslintrc.js
Normal file
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
studio/.gitignore
vendored
Normal file
6
studio/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
/package-lock.json
|
||||||
|
/yarn.lock
|
||||||
4
studio/.prettierrc
Normal file
4
studio/.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
1
studio/README.md
Normal file
1
studio/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# TestGatsbySanity-studio
|
||||||
6
studio/config/.checksums
Normal file
6
studio/config/.checksums
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
3
studio/config/@sanity/data-aspects.json
Normal file
3
studio/config/@sanity/data-aspects.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"listOptions": {}
|
||||||
|
}
|
||||||
6
studio/config/@sanity/default-layout.json
Normal file
6
studio/config/@sanity/default-layout.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"toolSwitcher": {
|
||||||
|
"order": [],
|
||||||
|
"hidden": []
|
||||||
|
}
|
||||||
|
}
|
||||||
7
studio/config/@sanity/default-login.json
Normal file
7
studio/config/@sanity/default-login.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"mode": "append",
|
||||||
|
"redirectOnSingle": false,
|
||||||
|
"entries": []
|
||||||
|
}
|
||||||
|
}
|
||||||
53
studio/dashboardConfig.js
Normal file
53
studio/dashboardConfig.js
Normal file
|
|
@ -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'}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
36
studio/deskStructure.js
Normal file
36
studio/deskStructure.js
Normal file
|
|
@ -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)
|
||||||
|
])
|
||||||
4
studio/netlify.toml
Normal file
4
studio/netlify.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "/"
|
||||||
|
status = 200
|
||||||
43
studio/package.json
Normal file
43
studio/package.json
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "TestGatsbySanity-studio",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "package.json",
|
||||||
|
"author": "Sanity <hello@sanity.io>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
studio/plugins/dashboard-widget-structure-menu/sanity.json
Normal file
13
studio/plugins/dashboard-widget-structure-menu/sanity.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"paths": {
|
||||||
|
"source": "./src",
|
||||||
|
"compiled": "./lib"
|
||||||
|
},
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"name": "part:@sanity/dashboard/widget/create",
|
||||||
|
"implements": "part:@sanity/dashboard/widget",
|
||||||
|
"path": "widget.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<h3 className={styles.title}>Edit your content</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
{props.structure.items.map(item => {
|
||||||
|
const Icon = getIconComponent(item)
|
||||||
|
return (
|
||||||
|
<div key={item.id}>
|
||||||
|
<Link className={styles.link} href={`/desk/${item.id}`}>
|
||||||
|
<div className={styles.iconWrapper}>
|
||||||
|
<Icon />
|
||||||
|
</div>
|
||||||
|
<div>{item.title}</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StructureMenuWidget
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export {default as StructureMenuWidget} from './StructureMenuWidget'
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
11
studio/plugins/dashboard-widget-structure-menu/src/props.js
Normal file
11
studio/plugins/dashboard-widget-structure-menu/src/props.js
Normal file
|
|
@ -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}))
|
||||||
|
)
|
||||||
|
}
|
||||||
10
studio/plugins/dashboard-widget-structure-menu/src/widget.js
Normal file
10
studio/plugins/dashboard-widget-structure-menu/src/widget.js
Normal file
|
|
@ -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'}
|
||||||
|
}
|
||||||
35
studio/sanity.json
Normal file
35
studio/sanity.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
studio/schemas/documents/category.js
Normal file
17
studio/schemas/documents/category.js
Normal file
|
|
@ -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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
studio/schemas/documents/person.js
Normal file
41
studio/schemas/documents/person.js
Normal file
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
studio/schemas/documents/project.js
Normal file
90
studio/schemas/documents/project.js
Normal file
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
studio/schemas/documents/siteSettings.js
Normal file
35
studio/schemas/documents/siteSettings.js
Normal file
|
|
@ -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'}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
studio/schemas/objects/bioPortableText.js
Normal file
34
studio/schemas/objects/bioPortableText.js
Normal file
|
|
@ -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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
studio/schemas/objects/figure.js
Normal file
34
studio/schemas/objects/figure.js
Normal file
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
studio/schemas/objects/projectMember.js
Normal file
42
studio/schemas/objects/projectMember.js
Normal file
|
|
@ -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('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
studio/schemas/objects/projectPortableText.js
Normal file
51
studio/schemas/objects/projectPortableText.js
Normal file
|
|
@ -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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
39
studio/schemas/objects/simplePortableText.js
Normal file
39
studio/schemas/objects/simplePortableText.js
Normal file
|
|
@ -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: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
studio/schemas/schema.js
Normal file
41
studio/schemas/schema.js
Normal file
|
|
@ -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
|
||||||
|
])
|
||||||
|
})
|
||||||
1
studio/static/.gitkeep
Normal file
1
studio/static/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Files placed here will be served by the Sanity server under the `/static`-prefix
|
||||||
BIN
studio/static/favicon.ico
Normal file
BIN
studio/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
6
web/.env.development.template
Normal file
6
web/.env.development.template
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
GATSBY_SANITY_PROJECT_ID="dkwlsoid"
|
||||||
|
GATSBY_SANITY_DATASET="production"
|
||||||
|
|
||||||
|
# Rename this file to .env.development for local development.
|
||||||
|
# Note: After renaming – DO NOT commit this file to git!
|
||||||
|
SANITY_READ_TOKEN=""
|
||||||
2
web/.env.production
Normal file
2
web/.env.production
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
GATSBY_SANITY_PROJECT_ID="dkwlsoid"
|
||||||
|
GATSBY_SANITY_DATASET="production"
|
||||||
13
web/.eslintrc.js
Normal file
13
web/.eslintrc.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: ['standard', 'standard-react', 'plugin:import/errors', 'plugin:import/warnings'],
|
||||||
|
rules: {
|
||||||
|
'react/prop-types': 0,
|
||||||
|
'object-curly-spacing': ['error', 'never']
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
pragma: 'React',
|
||||||
|
version: '16.8.4'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
web/.gitignore
vendored
Normal file
75
web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# gatsby files
|
||||||
|
.cache/
|
||||||
|
public
|
||||||
|
|
||||||
|
# Mac files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Yarn
|
||||||
|
yarn-error.log
|
||||||
|
yarn.lock
|
||||||
|
.pnp/
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
/package-lock.json
|
||||||
|
/yarn.lock
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env.development
|
||||||
3
web/.prettierrc
Normal file
3
web/.prettierrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
1
web/README.md
Normal file
1
web/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# TestGatsbySanity-web
|
||||||
6
web/client-config.js
Normal file
6
web/client-config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
sanity: {
|
||||||
|
projectId: process.env.GATSBY_SANITY_PROJECT_ID || 'dkwlsoid',
|
||||||
|
dataset: process.env.GATSBY_SANITY_DATASET || 'production'
|
||||||
|
}
|
||||||
|
}
|
||||||
7
web/gatsby-browser.js
Normal file
7
web/gatsby-browser.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Implement Gatsby's Browser APIs in this file.
|
||||||
|
*
|
||||||
|
* See: https://www.gatsbyjs.org/docs/browser-apis/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// You can delete this file if you're not using it
|
||||||
25
web/gatsby-config.js
Normal file
25
web/gatsby-config.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Load variables from `.env` as soon as possible
|
||||||
|
require('dotenv').config({
|
||||||
|
path: `.env.${process.env.NODE_ENV || 'development'}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const clientConfig = require('./client-config')
|
||||||
|
const token = process.env.SANITY_READ_TOKEN
|
||||||
|
|
||||||
|
const isProd = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
'gatsby-plugin-postcss',
|
||||||
|
'gatsby-plugin-react-helmet',
|
||||||
|
{
|
||||||
|
resolve: 'gatsby-source-sanity',
|
||||||
|
options: {
|
||||||
|
...clientConfig.sanity,
|
||||||
|
token,
|
||||||
|
watchMode: !isProd,
|
||||||
|
overlayDrafts: !isProd && token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
51
web/gatsby-node.js
Normal file
51
web/gatsby-node.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
const {isFuture} = require('date-fns')
|
||||||
|
/**
|
||||||
|
* Implement Gatsby's Node APIs in this file.
|
||||||
|
*
|
||||||
|
* See: https://www.gatsbyjs.org/docs/node-apis/
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function createProjectPages (graphql, actions, reporter) {
|
||||||
|
const {createPage, createPageDependency} = actions
|
||||||
|
const result = await graphql(`
|
||||||
|
{
|
||||||
|
allSanityProject(filter: {slug: {current: {ne: null}}, publishedAt: {ne: null}}) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
publishedAt
|
||||||
|
slug {
|
||||||
|
current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
if (result.errors) throw result.errors
|
||||||
|
|
||||||
|
const projectEdges = (result.data.allSanityProject || {}).edges || []
|
||||||
|
|
||||||
|
projectEdges
|
||||||
|
.filter(edge => !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)
|
||||||
|
}
|
||||||
7
web/gatsby-ssr.js
Normal file
7
web/gatsby-ssr.js
Normal file
|
|
@ -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
|
||||||
42
web/package.json
Normal file
42
web/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "TestGatsbySanity-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Sanity <hello@sanity.io>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
web/postcss.config.js
Normal file
8
web/postcss.config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = () => ({
|
||||||
|
plugins: [
|
||||||
|
require('postcss-import'),
|
||||||
|
require('postcss-preset-env')({
|
||||||
|
stage: 0
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
10
web/src/components/block-content.js
Normal file
10
web/src/components/block-content.js
Normal file
|
|
@ -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}) => (
|
||||||
|
<BaseBlockContent blocks={blocks} serializers={serializers} {...clientConfig.sanity} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export default BlockContent
|
||||||
7
web/src/components/block-text.js
Normal file
7
web/src/components/block-text.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import BaseBlockContent from '@sanity/block-content-to-react'
|
||||||
|
import React from 'react'
|
||||||
|
import serializers from './serializers'
|
||||||
|
|
||||||
|
const BlockText = ({blocks}) => <BaseBlockContent blocks={blocks} serializers={serializers} />
|
||||||
|
|
||||||
|
export default BlockText
|
||||||
9
web/src/components/container.js
Normal file
9
web/src/components/container.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import styles from './container.module.css'
|
||||||
|
|
||||||
|
const Container = ({children}) => {
|
||||||
|
return <div className={styles.root}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Container
|
||||||
12
web/src/components/container.module.css
Normal file
12
web/src/components/container.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
web/src/components/figure.js
Normal file
21
web/src/components/figure.js
Normal file
|
|
@ -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 (
|
||||||
|
<figure className={styles.root}>
|
||||||
|
<Img fluid={fluidProps} alt={node.alt} />
|
||||||
|
{node.caption && <figcaption>{node.caption}</figcaption>}
|
||||||
|
</figure>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
web/src/components/figure.module.css
Normal file
11
web/src/components/figure.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
web/src/components/graphql-error-list.js
Normal file
12
web/src/components/graphql-error-list.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const GraphQLErrorList = ({errors}) => (
|
||||||
|
<div>
|
||||||
|
<h1>GraphQL Error</h1>
|
||||||
|
{errors.map(error => (
|
||||||
|
<pre key={error.message}>{error.message}</pre>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default GraphQLErrorList
|
||||||
30
web/src/components/header.js
Normal file
30
web/src/components/header.js
Normal file
|
|
@ -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}) => (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<div className={styles.branding}>
|
||||||
|
<Link to='/'>{siteTitle}</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className={styles.toggleNavButton} onClick={showNav ? onHideNav : onShowNav}>
|
||||||
|
<Icon symbol='hamburger' />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nav className={cn(styles.nav, showNav && styles.showNav)}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<Link to='/archive/'>Archive</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Header
|
||||||
114
web/src/components/header.module.css
Normal file
114
web/src/components/header.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
20
web/src/components/icon/hamburger.js
Normal file
20
web/src/components/icon/hamburger.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const strokeStyle = {vectorEffect: 'non-scaling-stroke'}
|
||||||
|
|
||||||
|
const HamburgerIcon = () => (
|
||||||
|
<svg
|
||||||
|
viewBox='0 0 25 25'
|
||||||
|
fill='none'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
preserveAspectRatio='xMidYMid'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
>
|
||||||
|
<path d='M5 7.5H20' stroke='currentColor' style={strokeStyle} />
|
||||||
|
<path d='M5 12.5H20' stroke='currentColor' style={strokeStyle} />
|
||||||
|
<path d='M5 17.5H20' stroke='currentColor' style={strokeStyle} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default HamburgerIcon
|
||||||
13
web/src/components/icon/index.js
Normal file
13
web/src/components/icon/index.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react'
|
||||||
|
import HamburgerIcon from './hamburger'
|
||||||
|
|
||||||
|
function Icon (props) {
|
||||||
|
switch (props.symbol) {
|
||||||
|
case 'hamburger':
|
||||||
|
return <HamburgerIcon />
|
||||||
|
default:
|
||||||
|
return <span>Unknown icon: {props.symbol}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Icon
|
||||||
23
web/src/components/layout.js
Normal file
23
web/src/components/layout.js
Normal file
|
|
@ -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}) => (
|
||||||
|
<>
|
||||||
|
<Header siteTitle={siteTitle} onHideNav={onHideNav} onShowNav={onShowNav} showNav={showNav} />
|
||||||
|
<div className={styles.content}>{children}</div>
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<div className={styles.footerWrapper}>
|
||||||
|
<div className={styles.siteInfo}>
|
||||||
|
© {new Date().getFullYear()}, Built with <a href='https://www.sanity.io'>Sanity</a> &
|
||||||
|
{` `}
|
||||||
|
<a href='https://www.gatsbyjs.org'>Gatsby</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Layout
|
||||||
48
web/src/components/layout.module.css
Normal file
48
web/src/components/layout.module.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
34
web/src/components/project-preview-grid.js
Normal file
34
web/src/components/project-preview-grid.js
Normal file
|
|
@ -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 (
|
||||||
|
<div className={styles.root}>
|
||||||
|
{props.title && <h2 className={styles.headline}>{props.title}</h2>}
|
||||||
|
<ul className={styles.grid}>
|
||||||
|
{props.nodes &&
|
||||||
|
props.nodes.map(node => (
|
||||||
|
<li key={node.id}>
|
||||||
|
<ProjectPreview {...node} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{props.browseMoreHref && (
|
||||||
|
<div className={styles.browseMoreNav}>
|
||||||
|
<Link to={props.browseMoreHref}>Browse more</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectPreviewGrid.defaultProps = {
|
||||||
|
title: '',
|
||||||
|
nodes: [],
|
||||||
|
browseMoreHref: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectPreviewGrid
|
||||||
51
web/src/components/project-preview-grid.module.css
Normal file
51
web/src/components/project-preview-grid.module.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
web/src/components/project-preview.js
Normal file
34
web/src/components/project-preview.js
Normal file
|
|
@ -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 (
|
||||||
|
<Link className={styles.root} to={`/project/${props.slug.current}`}>
|
||||||
|
<div className={styles.leadMediaThumb}>
|
||||||
|
{props.mainImage && props.mainImage.asset && (
|
||||||
|
<img
|
||||||
|
src={imageUrlFor(buildImageObj(props.mainImage))
|
||||||
|
.width(600)
|
||||||
|
.height(Math.floor((9 / 16) * 600))
|
||||||
|
.url()}
|
||||||
|
alt={props.mainImage.alt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h3 className={cn(responsiveTitle3, styles.title)}>{props.title}</h3>
|
||||||
|
{props._rawExcerpt && (
|
||||||
|
<div className={styles.excerpt}>
|
||||||
|
<BlockText blocks={props._rawExcerpt} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectPreview
|
||||||
42
web/src/components/project-preview.module.css
Normal file
42
web/src/components/project-preview.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
web/src/components/project.js
Normal file
76
web/src/components/project.js
Normal file
|
|
@ -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 (
|
||||||
|
<article className={styles.root}>
|
||||||
|
{props.mainImage && mainImage.asset && (
|
||||||
|
<div className={styles.mainImage}>
|
||||||
|
<img
|
||||||
|
src={imageUrlFor(buildImageObj(mainImage))
|
||||||
|
.width(1200)
|
||||||
|
.height(Math.floor((9 / 16) * 1200))
|
||||||
|
.fit('crop')
|
||||||
|
.url()}
|
||||||
|
alt={mainImage.alt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Container>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
<h1 className={styles.title}>{title}</h1>
|
||||||
|
{_rawBody && <BlockContent blocks={_rawBody || []} />}
|
||||||
|
</div>
|
||||||
|
<aside className={styles.metaContent}>
|
||||||
|
{publishedAt && (
|
||||||
|
<div className={styles.publishedAt}>
|
||||||
|
{differenceInDays(new Date(publishedAt), new Date()) > 3
|
||||||
|
? distanceInWords(new Date(publishedAt), new Date())
|
||||||
|
: format(new Date(publishedAt), 'MMMM Do YYYY')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{members && members.length > 0 && <RoleList items={members} title='Project members' />}
|
||||||
|
{categories && categories.length > 0 && (
|
||||||
|
<div className={styles.categories}>
|
||||||
|
<h3 className={styles.categoriesHeadline}>Categories</h3>
|
||||||
|
<ul>
|
||||||
|
{categories.map(category => (
|
||||||
|
<li key={category._id}>{category.title}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{relatedProjects && relatedProjects.length > 0 && (
|
||||||
|
<div className={styles.relatedProjects}>
|
||||||
|
<h3 className={styles.relatedProjectsHeadline}>Related projects</h3>
|
||||||
|
<ul>
|
||||||
|
{relatedProjects.map(project => (
|
||||||
|
<li key={`related_${project._id}`}>
|
||||||
|
{project.slug ? (
|
||||||
|
<Link to={`/project/${project.slug.current}`}>{project.title}</Link>
|
||||||
|
) : (
|
||||||
|
<span>{project.title}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Project
|
||||||
98
web/src/components/project.module.css
Normal file
98
web/src/components/project.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
55
web/src/components/role-list.js
Normal file
55
web/src/components/role-list.js
Normal file
|
|
@ -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 (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<h2 className={styles.headline}>{title}</h2>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{items.map(item => (
|
||||||
|
<li key={item._key} className={styles.listItem}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.avatar}>
|
||||||
|
{item.person && item.person.image && item.person.image.asset && (
|
||||||
|
<img
|
||||||
|
src={imageUrlFor(buildImageObj(item.person.image))
|
||||||
|
.width(100)
|
||||||
|
.height(100)
|
||||||
|
.fit('crop')
|
||||||
|
.url()}
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<strong>{(item.person && item.person.name) || <em>Missing name</em>}</strong>
|
||||||
|
</div>
|
||||||
|
{item.roles && (
|
||||||
|
<div>
|
||||||
|
{item.roles.map((role, idx) => {
|
||||||
|
switch (true) {
|
||||||
|
case idx === 0:
|
||||||
|
return <span key={role}>{ucfirst(role)}</span>
|
||||||
|
case idx === item.roles.length - 1:
|
||||||
|
return <span key={role}> & {role}</span>
|
||||||
|
default:
|
||||||
|
return <span key={role}>, {role}</span>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RoleList
|
||||||
46
web/src/components/role-list.module.css
Normal file
46
web/src/components/role-list.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
web/src/components/seo.js
Normal file
96
web/src/components/seo.js
Normal file
|
|
@ -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 (
|
||||||
|
<StaticQuery
|
||||||
|
query={detailsQuery}
|
||||||
|
render={data => {
|
||||||
|
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 (
|
||||||
|
<Helmet
|
||||||
|
htmlAttributes={{lang}}
|
||||||
|
title={title}
|
||||||
|
titleTemplate={title === siteTitle ? '%s' : `%s | ${siteTitle}`}
|
||||||
|
meta={[
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
content: metaDescription
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: 'og:title',
|
||||||
|
content: title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: 'og:description',
|
||||||
|
content: metaDescription
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: 'og:type',
|
||||||
|
content: 'website'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'twitter:card',
|
||||||
|
content: 'summary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'twitter:creator',
|
||||||
|
content: siteAuthor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'twitter:title',
|
||||||
|
content: title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'twitter:description',
|
||||||
|
content: metaDescription
|
||||||
|
}
|
||||||
|
]
|
||||||
|
.concat(
|
||||||
|
keywords && keywords.length > 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
9
web/src/components/serializers.js
Normal file
9
web/src/components/serializers.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Figure from './figure'
|
||||||
|
|
||||||
|
const serializers = {
|
||||||
|
types: {
|
||||||
|
figure: Figure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default serializers
|
||||||
156
web/src/components/typography.module.css
Normal file
156
web/src/components/typography.module.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
web/src/containers/layout.js
Normal file
44
web/src/containers/layout.js
Normal file
|
|
@ -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 (
|
||||||
|
<StaticQuery
|
||||||
|
query={query}
|
||||||
|
render={data => {
|
||||||
|
if (!data.site) {
|
||||||
|
throw new Error(
|
||||||
|
'Missing "Site settings". Open the studio at http://localhost:3333 and add "Site settings" data'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
{...props}
|
||||||
|
showNav={showNav}
|
||||||
|
siteTitle={data.site.title}
|
||||||
|
onHideNav={handleHideNav}
|
||||||
|
onShowNav={handleShowNav}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutContainer
|
||||||
33
web/src/lib/helpers.js
Normal file
33
web/src/lib/helpers.js
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
8
web/src/lib/image-url.js
Normal file
8
web/src/lib/image-url.js
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
3
web/src/lib/string-utils.js
Normal file
3
web/src/lib/string-utils.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function ucfirst (str) {
|
||||||
|
return `${str.substr(0, 1).toUpperCase()}${str.substr(1)}`
|
||||||
|
}
|
||||||
14
web/src/pages/404.js
Normal file
14
web/src/pages/404.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Layout from '../components/layout'
|
||||||
|
import SEO from '../components/seo'
|
||||||
|
|
||||||
|
const NotFoundPage = () => (
|
||||||
|
<Layout>
|
||||||
|
<SEO title='404: Not found' />
|
||||||
|
<h1>NOT FOUND</h1>
|
||||||
|
<p>You just hit a route that doesn't exist... the sadness.</p>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default NotFoundPage
|
||||||
61
web/src/pages/archive.js
Normal file
61
web/src/pages/archive.js
Normal file
|
|
@ -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 (
|
||||||
|
<Layout>
|
||||||
|
<GraphQLErrorList errors={errors} />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const projectNodes =
|
||||||
|
data && data.projects && mapEdgesToNodes(data.projects).filter(filterOutDocsWithoutSlugs)
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<SEO title='Archive' />
|
||||||
|
<Container>
|
||||||
|
<h1 className={responsiveTitle1}>Projects</h1>
|
||||||
|
{projectNodes && projectNodes.length > 0 && <ProjectPreviewGrid nodes={projectNodes} />}
|
||||||
|
</Container>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArchivePage
|
||||||
103
web/src/pages/index.js
Normal file
103
web/src/pages/index.js
Normal file
|
|
@ -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 (
|
||||||
|
<Layout>
|
||||||
|
<GraphQLErrorList errors={errors} />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Layout>
|
||||||
|
<SEO title={site.title} description={site.description} keywords={site.keywords} />
|
||||||
|
<Container>
|
||||||
|
<h1 hidden>Welcome to {site.title}</h1>
|
||||||
|
{projectNodes && (
|
||||||
|
<ProjectPreviewGrid
|
||||||
|
title='Latest projects'
|
||||||
|
nodes={projectNodes}
|
||||||
|
browseMoreHref='/archive/'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexPage
|
||||||
4
web/src/styles/custom-media.css
Normal file
4
web/src/styles/custom-media.css
Normal file
|
|
@ -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);
|
||||||
29
web/src/styles/custom-properties.css
Normal file
29
web/src/styles/custom-properties.css
Normal file
|
|
@ -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 */
|
||||||
|
}
|
||||||
21
web/src/styles/layout.css
Normal file
21
web/src/styles/layout.css
Normal file
|
|
@ -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%;
|
||||||
|
}
|
||||||
102
web/src/templates/project.js
Normal file
102
web/src/templates/project.js
Normal file
|
|
@ -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 (
|
||||||
|
<Layout>
|
||||||
|
{errors && <SEO title='GraphQL Error' />}
|
||||||
|
{project && <SEO title={project.title || 'Untitled'} />}
|
||||||
|
|
||||||
|
{errors && (
|
||||||
|
<Container>
|
||||||
|
<GraphQLErrorList errors={errors} />
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
{project && <Project {...project} />}
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectTemplate
|
||||||
Loading…
Add table
Add a link
Reference in a new issue