Initial commit
This commit is contained in:
commit
5a3bf52ebb
86 changed files with 2639 additions and 0 deletions
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