Initial commit

This commit is contained in:
Waylon Walker 2019-05-09 10:15:41 -05:00
commit 5a3bf52ebb
86 changed files with 2639 additions and 0 deletions

View 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

View 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

View 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

View 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;
}
}

View 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>
)
}

View 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;
}
}

View 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

View 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

View 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;
}

View 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

View 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

View 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

View 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);
}

View 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

View 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);
}
}
}
}

View 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

View 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;
}
}

View 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

View 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;
}

View 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

View 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
View 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
}
}
}
`

View file

@ -0,0 +1,9 @@
import Figure from './figure'
const serializers = {
types: {
figure: Figure
}
}
export default serializers

View 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);
}
}

View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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

View 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);

View 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
View 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%;
}

View 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