docs: convert website from postcss to emotion (#2068)

This commit is contained in:
Zeb Pykosz
2020-01-23 21:55:18 -05:00
committed by Shawn Erquhart
parent 7b0838dfef
commit 3d474b1944
69 changed files with 7769 additions and 6834 deletions

View File

@ -0,0 +1,34 @@
import { css } from '@emotion/core';
import styled from '@emotion/styled';
import theme from '../theme';
// prettier-ignore
const Button = styled.button`
display: inline-block;
background-image: linear-gradient(0deg, #97bf2f 14%, #c9fa4b 94%);
color: ${theme.colors.darkGray};
border-radius: ${theme.radii[1]};
font-size: ${theme.fontsize[3]};
font-weight: 700;
padding: ${theme.space[2]} ${theme.space[3]};
border: 2px solid ${theme.colors.darkGreen};
cursor: pointer;
${p => p.block && css`
display: block;
width: 100%;
`};
${p => p.outline && css`
background: none;
font-weight: 500;
`};
${p => p.active && css`
background: ${theme.colors.darkGreen};
color: white;
`};
`;
export default Button;

View File

@ -0,0 +1,18 @@
import React from 'react';
import styled from '@emotion/styled';
const ChatLink = styled.a`
z-index: 100;
position: fixed;
bottom: 10px;
right: 10px;
cursor: pointer;
`;
const ChatButton = () => (
<ChatLink href="/chat">
<img src="/img/slack.svg" />
</ChatLink>
);
export default ChatButton;

View File

@ -0,0 +1,56 @@
import React from 'react';
import styled from '@emotion/styled';
import theme from '../theme';
const StyledCommunityChannelsList = styled.ul`
margin-left: 0;
li {
list-style-type: none;
margin-bottom: 24px;
}
a {
display: block;
font-weight: inherit;
position: relative;
&:focus,
&:active,
&:hover {
&:before {
display: block;
}
}
&:before {
display: none;
content: '';
position: absolute;
width: 3px;
height: 100%;
background-color: ${theme.colors.darkGreen};
left: -16px;
}
}
p {
color: ${theme.colors.gray};
margin-bottom: 0;
}
`;
const CommunityChannelsList = ({ channels }) => (
<StyledCommunityChannelsList>
{channels.map(({ title, description, url }, idx) => (
<li key={idx}>
<a href={url}>
<strong>{title}</strong>
<p>{description}</p>
</a>
</li>
))}
</StyledCommunityChannelsList>
);
export default CommunityChannelsList;

View File

@ -0,0 +1,32 @@
import styled from '@emotion/styled';
import { css } from '@emotion/core';
import { mq } from '../utils';
import theme from '../theme';
const Container = styled.div`
margin-left: auto;
margin-right: auto;
max-width: 1280px;
padding-left: ${theme.space[4]};
padding-right: ${theme.space[4]};
${p =>
p.size === 'sm' &&
css`
max-width: 800px;
`};
${p =>
p.size === 'md' &&
css`
max-width: 1024px;
`};
${mq[3]} {
padding-left: ${theme.space[5]};
padding-right: ${theme.space[5]};
}
`;
export default Container;

View File

@ -1,67 +1,101 @@
import React, { Component } from 'react';
import Link from 'gatsby-link';
import React, { useState } from 'react';
import { Link } from 'gatsby';
import styled from '@emotion/styled';
/**
* Manually get table of contents since tableOfContents from markdown
* nodes have code added.
*
* https://github.com/gatsbyjs/gatsby/issues/5436
*/
class TableOfContents extends Component {
state = {
headings: [],
import Button from './button';
import TableOfContents from './table-of-contents';
import { mq } from '../utils';
import theme from '../theme';
const Menu = styled.nav`
margin-bottom: ${theme.space[5]};
`;
const MenuBtn = styled(Button)`
${mq[1]} {
display: none;
}
`;
const MenuContent = styled.div`
display: ${p => (p.isOpen ? 'block' : 'none')};
background: white;
padding: ${theme.space[3]};
${mq[1]} {
display: block;
background: transparent;
padding: 0;
}
`;
const MenuSection = styled.div`
margin-bottom: ${theme.space[3]};
`;
const SectionTitle = styled.h3`
font-size: ${theme.fontsize[4]};
margin-bottom: ${theme.space[2]};
`;
const SectionList = styled.ul``;
const MenuItem = styled.li``;
const NavLink = styled(Link)`
display: block;
/* font-weight: $regular; */
font-size: ${theme.fontsize[3]};
color: ${theme.colors.gray};
line-height: ${theme.lineHeight[1]};
text-transform: capitalize;
transition: color 0.2s ease;
padding: ${theme.space[2]} 0;
&.active {
color: ${theme.colors.darkGreen};
font-weight: bold;
}
&:hover {
color: ${theme.colors.darkGreen};
}
`;
const DocsNav = ({ items, location }) => {
const [isMenuOpen, setMenuOpen] = useState(false);
const toggleMenu = () => {
setMenuOpen(isOpen => !isOpen);
};
componentDidMount() {
const contentHeadings = document.querySelectorAll('.docs-content h2');
const headings = [];
contentHeadings.forEach(h => {
headings.push({
id: h.id,
text: h.innerText,
});
});
this.setState({
headings,
});
}
render() {
const { headings } = this.state;
return (
<ul className="nav-subsections">
{headings.map(h => (
<li key={h.id}>
<a href={`#${h.id}`} className="subnav-link">
{h.text}
</a>
</li>
return (
<Menu>
<MenuBtn onClick={toggleMenu} block>
{isMenuOpen ? <span>&times;</span> : <span>&#9776;</span>} {isMenuOpen ? 'Hide' : 'Show'}{' '}
Navigation
</MenuBtn>
<MenuContent isOpen={isMenuOpen}>
{items.map(item => (
<MenuSection key={item.title}>
<SectionTitle>{item.title}</SectionTitle>
<SectionList>
{item.group.edges.map(({ node }) => (
<MenuItem key={node.fields.slug}>
<NavLink to={node.fields.slug} activeClassName="active">
{node.frontmatter.title}
</NavLink>
{location.pathname === node.fields.slug && <TableOfContents />}
</MenuItem>
))}
</SectionList>
</MenuSection>
))}
</ul>
);
}
}
const DocsNav = ({ items, location }) => (
<nav className="docs-nav" id="docs-nav">
{items.map(item => (
<div className="docs-nav-section" key={item.title}>
<div className="docs-nav-section-title">{item.title}</div>
<ul className="docs-nav-section-list">
{item.group.edges.map(({ node }) => (
<li className="docs-nav-item" key={node.fields.slug}>
<Link to={node.fields.slug} className="nav-link" activeClassName="active">
{node.frontmatter.title}
</Link>
{location.pathname === node.fields.slug && <TableOfContents />}
</li>
))}
</ul>
</div>
))}
</nav>
);
</MenuContent>
</Menu>
);
};
export default DocsNav;
export { NavLink };

View File

@ -1,35 +1,56 @@
import React, { Component } from 'react';
import React, { useState, useEffect, memo } from 'react';
import styled from '@emotion/styled';
import theme from '../theme';
import searchIcon from '../img/search.svg';
class DocSearch extends Component {
state = {
enabled: true,
};
componentDidMount() {
const SearchForm = styled.form`
> span {
width: 100%;
}
`;
const SearchField = styled.input`
color: white;
font-size: ${theme.fontsize[3]};
border-radius: ${theme.radii[1]};
background-color: rgba(255, 255, 255, 0.1);
background-image: url(${searchIcon});
background-repeat: no-repeat;
background-position: ${theme.space[2]} 50%;
border: 0;
appearance: none;
width: 100%;
padding: ${theme.space[2]};
padding-left: 30px;
outline: 0;
`;
const DocSearch = () => {
const [enabled, setEnabled] = useState(true);
useEffect(() => {
if (window.docsearch) {
window.docsearch({
apiKey: '08d03dc80862e84c70c5a1e769b13019',
indexName: 'netlifycms',
inputSelector: '.algolia-search',
inputSelector: '#algolia-search',
debug: false, // Set debug to true if you want to inspect the dropdown
});
} else {
this.setState({ enabled: false });
}
}
render() {
if (!this.state.enabled) {
return null;
setEnabled(false);
}
}, []);
return (
<div className="utility-input">
<img src={searchIcon} alt="" />
<input type="search" placeholder="Search the docs" className="algolia-search" />
</div>
);
if (!enabled) {
return null;
}
}
export default DocSearch;
return (
<SearchForm>
<SearchField type="search" placeholder="Search the docs" id="algolia-search" />
</SearchForm>
);
};
export default memo(DocSearch);

View File

@ -1,32 +1,44 @@
import React from 'react';
import { css } from '@emotion/core';
const EditLink = ({ path }) => (
<a
className="edit-this-page"
href={`https://github.com/netlify/netlify-cms/blob/master/website/content/${path}`}
<div
css={css`
float: right;
a {
font-weight: 700;
}
#pencil {
fill: #7ca511;
}
`}
>
<svg
version="1.1"
id="pencil"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="14px"
height="14px"
viewBox="0 0 512 512"
enableBackground="new 0 0 512 512"
xmlSpace="preserve"
>
<path
d="M398.875,248.875L172.578,475.187l-22.625-22.625L376.25,226.265L398.875,248.875z M308.375,158.39L82.063,384.687
<a href={`https://github.com/netlify/netlify-cms/blob/master/website/content/${path}`}>
<svg
version="1.1"
id="pencil"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="14px"
height="14px"
viewBox="0 0 512 512"
enableBackground="new 0 0 512 512"
xmlSpace="preserve"
>
<path
d="M398.875,248.875L172.578,475.187l-22.625-22.625L376.25,226.265L398.875,248.875z M308.375,158.39L82.063,384.687
l45.266,45.25L353.625,203.64L308.375,158.39z M263.094,113.125L36.828,339.437l22.625,22.625L285.75,135.765L263.094,113.125z
M308.375,67.875L285.719,90.5L421.5,226.265l22.625-22.625L308.375,67.875z M376.25,0L331,45.25l135.75,135.766L512,135.781
L376.25,0z M32,453.5V480h26.5L32,453.5 M0,376.25L135.766,512H0V376.25L0,376.25z"
/>
</svg>{' '}
Edit this page
</a>
/>
</svg>{' '}
Edit this page
</a>
</div>
);
export default EditLink;

View File

@ -0,0 +1,126 @@
import React, { useState, useEffect } from 'react';
import moment from 'moment';
import styled from '@emotion/styled';
import Markdownify from './markdownify';
import theme from '../theme';
const Root = styled.div`
text-align: center;
background: ${theme.colors.darkerGray};
background-image: linear-gradient(
-17deg,
${theme.colors.darkerGray} 17%,
${theme.colors.darkGray} 94%
);
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 24px;
padding-top: 40px;
max-width: 446px;
`;
const Title = styled.h2`
font-size: 36px;
color: white;
`;
const Cal = styled.div`
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
margin: 24px auto;
max-width: 250px;
`;
const Month = styled.div`
background: ${theme.colors.green};
color: ${theme.colors.gray};
font-weight: bold;
text-transform: uppercase;
letter-spacing: 4px;
font-size: 14px;
padding: 8px;
`;
const Day = styled.div`
font-size: 104px;
line-height: 1.3;
font-weight: bold;
color: white;
border: 1px solid ${theme.colors.gray};
border-top: none;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
`;
const CalDates = styled.p`
color: white;
font-weight: bold;
font-size: ${theme.fontsize[4]};
margin-bottom: ${theme.space[3]};
`;
const CalCta = styled.div``;
const EventBox = ({ title, cta }) => {
const [loading, setLoading] = useState(true);
const [eventDate, setEventDate] = useState('');
useEffect(() => {
const eventbriteToken = 'C5PX65CJBVIXWWLNFKLO';
const eventbriteOrganiser = '14281996019';
const url = `https://www.eventbriteapi.com/v3/events/search/?token=${eventbriteToken}&organizer.id=${eventbriteOrganiser}&expand=venue%27`;
fetch(url)
.then(res => res.json())
.then(data => {
const eventDate = data.events[0].start.utc;
setEventDate(eventDate);
setLoading(false);
})
.catch(err => {
console.log(err); // eslint-disable-line no-console
// TODO: set state to show error message
setLoading(false);
});
}, []);
const eventDateMoment = moment(eventDate);
const offset = eventDateMoment.isDST() ? -7 : -8;
const month = eventDateMoment.format('MMMM');
const day = eventDateMoment.format('DD');
const datePrefix = eventDateMoment.format('dddd, MMMM Do');
const dateSuffix = eventDateMoment.utcOffset(offset).format('h a');
const ellip = <span>&hellip;</span>;
return (
<Root>
<Title>{title}</Title>
<Cal>
<Month>{loading ? 'loading' : month}</Month>
<Day>{loading ? ellip : day}</Day>
</Cal>
<CalDates>
{loading ? (
ellip
) : (
<span>
{datePrefix} at {dateSuffix} PT
</span>
)}
</CalDates>
<CalCta>
<Markdownify source={cta} />
</CalCta>
</Root>
);
};
export default EventBox;

View File

@ -1,71 +0,0 @@
import React, { Component } from 'react';
import moment from 'moment';
class EventWidget extends Component {
state = {
loading: false,
eventDate: '',
};
componentDidMount() {
const eventbriteToken = 'C5PX65CJBVIXWWLNFKLO';
const eventbriteOrganiser = '14281996019';
const url = `https://www.eventbriteapi.com/v3/events/search/?token=${eventbriteToken}&organizer.id=${eventbriteOrganiser}&expand=venue%27`;
this.setState({
loading: true,
});
fetch(url)
.then(res => res.json())
.then(data => {
const eventDate = data.events[0].start.utc;
this.setState({
loading: false,
eventDate,
});
})
.catch(err => {
console.log(err); // eslint-disable-line no-console
// TODO: set state to show error message
this.setState({
loading: false,
});
});
}
render() {
const { loading, eventDate } = this.state;
if (loading) {
return <span>Loading...</span>;
}
const eventDateMoment = moment(eventDate);
const offset = eventDateMoment.isDST() ? -7 : -8;
const month = eventDateMoment.format('MMMM');
const day = eventDateMoment.format('DD');
const datePrefix = eventDateMoment.format('dddd, MMMM Do');
const dateSuffix = eventDateMoment.utcOffset(offset).format('h a');
return (
<div>
<div className="calendar">
<div className="month">{month}</div>
<div className="day">{day}</div>
</div>
<h3>
<strong>
{datePrefix} at {dateSuffix} PT
</strong>
</h3>
</div>
);
}
}
export default EventWidget;

View File

@ -0,0 +1,43 @@
import React from 'react';
import styled from '@emotion/styled';
import Markdownify from './markdownify';
import theme from '../theme';
const Box = styled.div`
margin-bottom: ${theme.space[5]};
img {
margin-bottom: ${theme.space[3]};
margin-left: -${theme.space[2]};
}
`;
const Title = styled.h3`
color: ${p => (p.kind === 'light' ? theme.colors.white : theme.colors.gray)};
font-size: ${theme.fontsize[4]};
`;
const Text = styled.p`
font-size: 18px;
a {
font-weight: 700;
}
`;
const FeatureItem = ({ feature, description, imgpath, kind }) => (
<Box>
{imgpath && <img src={require(`../img/${imgpath}`)} alt="" />}
<Title kind={kind}>
<Markdownify source={feature} />
</Title>
<Text>
<Markdownify source={description} />
</Text>
</Box>
);
const Features = ({ items, kind }) =>
items.map(item => <FeatureItem kind={kind} {...item} key={item.feature} />);
export default Features;

View File

@ -1,36 +1,96 @@
import React from 'react';
import styled from '@emotion/styled';
import '../css/imports/footer.css';
import Container from './container';
import theme from '../theme';
import { mq } from '../utils';
const Root = styled.footer`
background: white;
padding-top: ${theme.space[4]};
padding-bottom: ${theme.space[5]};
`;
const FooterGrid = styled.div`
text-align: center;
${mq[2]} {
display: flex;
align-items: center;
text-align: left;
}
`;
const FooterButtons = styled.div`
margin-bottom: ${theme.space[3]};
${mq[2]} {
margin-bottom: 0;
}
`;
const SocialButton = styled.a`
display: inline-block;
padding: ${theme.space[1]} ${theme.space[3]};
background-color: ${theme.colors.lightishGray};
color: white;
font-weight: 700;
font-size: ${theme.fontsize[2]};
border-radius: ${theme.radii[1]};
margin-right: ${theme.space[2]};
&:active,
&:hover {
background-color: ${theme.colors.darkGreen};
}
`;
const Info = styled.div`
font-size: ${theme.fontsize[1]};
color: ${theme.colors.gray};
opacity: 0.5;
${mq[2]} {
padding-left: ${theme.space[4]};
}
a {
font-weight: 700;
color: ${theme.colors.gray};
}
`;
const Footer = ({ buttons }) => (
<footer>
<div className="contained">
<div className="social-buttons">
{buttons.map(btn => (
<a href={btn.url} key={btn.url}>
{btn.name}
</a>
))}
</div>
<div className="footer-info">
<p>
<a
href="https://github.com/netlify/netlify-cms/blob/master/LICENSE"
className="text-link"
>
Distributed under MIT License
</a>{' '}
·{' '}
<a
href="https://github.com/netlify/netlify-cms/blob/master/CODE_OF_CONDUCT.md"
className="text-link"
>
Code of Conduct
</a>
</p>
</div>
</div>
</footer>
<Root>
<Container>
<FooterGrid>
<FooterButtons>
{buttons.map(btn => (
<SocialButton href={btn.url} key={btn.url}>
{btn.name}
</SocialButton>
))}
</FooterButtons>
<Info>
<p>
<a
href="https://github.com/netlify/netlify-cms/blob/master/LICENSE"
className="text-link"
>
Distributed under MIT License
</a>{' '}
·{' '}
<a
href="https://github.com/netlify/netlify-cms/blob/master/CODE_OF_CONDUCT.md"
className="text-link"
>
Code of Conduct
</a>
</p>
</Info>
</FooterGrid>
</Container>
</Root>
);
export default Footer;

View File

@ -0,0 +1,14 @@
import styled from '@emotion/styled';
import { mq } from '../utils';
import theme from '../theme';
const Grid = styled.div`
${mq[2]} {
display: grid;
grid-template-columns: repeat(${p => p.cols}, 1fr);
grid-gap: ${theme.space[7]};
}
`;
export default Grid;

View File

@ -1,88 +1,215 @@
import React, { Component } from 'react';
import Link from 'gatsby-link';
import classnames from 'classnames';
import { Location } from '@reach/router';
import React, { useState, useEffect } from 'react';
import { Link } from 'gatsby';
import styled from '@emotion/styled';
import { css } from '@emotion/core';
import GitHubButton from 'react-github-btn';
import Container from './container';
import Notifications from './notifications';
import DocSearch from './docsearch';
import GitHubButton from './github-button';
import logo from '../img/netlify-cms-logo.svg';
import searchIcon from '../img/search.svg';
import '../css/imports/header.css';
import theme from '../theme';
import { mq } from '../utils';
class Header extends Component {
state = {
scrolled: false,
};
const StyledHeader = styled.header`
background: ${theme.colors.darkerGray};
padding-top: ${theme.space[3]};
padding-bottom: ${theme.space[3]};
transition: background 0.2s ease, padding 0.2s ease, box-shadow 0.2s ease;
async componentDidMount() {
${mq[2]} {
position: sticky;
top: 0;
width: 100%;
z-index: ${theme.zIndexes.header};
${p =>
p.hasHeroBelow &&
!p.scrolled &&
css`
background: #2a2c24;
padding-top: ${theme.space[5]};
padding-bottom: ${theme.space[5]};
`};
}
`;
const HeaderContainer = styled(Container)`
display: flex;
align-items: center;
flex-wrap: wrap;
`;
const Logo = styled.div`
flex: 1 0 50%;
${mq[1]} {
flex: 0 0 auto;
margin-right: ${theme.space[5]};
}
`;
const MenuActions = styled.div`
flex: 1 0 50%;
display: flex;
justify-content: flex-end;
${mq[1]} {
display: none;
}
`;
const MenuBtn = styled.button`
background: none;
border: 0;
color: white;
padding: ${theme.space[3]};
font-size: ${theme.fontsize[4]};
line-height: 1;
`;
const SearchBtn = styled(MenuBtn)``;
const ToggleArea = styled.div`
display: ${p => (p.open ? 'block' : 'none')};
flex: 1;
width: 100%;
margin-top: ${theme.space[3]};
${mq[1]} {
display: block;
width: auto;
margin-top: 0;
}
`;
const SearchBox = styled(ToggleArea)`
${mq[1]} {
flex: 1;
max-width: 200px;
margin-right: ${theme.space[3]};
}
`;
const Menu = styled(ToggleArea)`
${mq[1]} {
flex: 0 0 auto;
margin-left: auto;
}
`;
const MenuList = styled.ul`
${mq[1]} {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
`;
const MenuItem = styled.li`
margin-bottom: ${theme.space[3]};
${mq[1]} {
margin-bottom: 0;
&:not(:last-child) {
margin-right: ${theme.space[3]};
}
}
`;
const NavLink = styled(Link)`
color: white;
text-decoration: none;
font-weight: 600;
`;
const Header = ({ hasHeroBelow }) => {
const [scrolled, setScrolled] = useState(false);
const [isNavOpen, setNavOpen] = useState(false);
const [isSearchOpen, setSearchOpen] = useState(false);
useEffect(() => {
// TODO: use raf to throttle events
window.addEventListener('scroll', this.handleScroll);
}
window.addEventListener('scroll', handleScroll);
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
handleScroll = () => {
const handleScroll = () => {
const currentWindowPos = document.documentElement.scrollTop || document.body.scrollTop;
const scrolled = currentWindowPos > 0;
this.setState({
scrolled,
});
setScrolled(scrolled);
};
render() {
const { scrolled } = this.state;
const handleMenuBtnClick = () => {
setNavOpen(s => !s);
setSearchOpen(false);
};
return (
<Location>
{({ location }) => {
const isDocs = location.pathname.indexOf('docs') !== -1;
const isBlog = location.pathname.indexOf('blog') !== -1;
const handleSearchBtnClick = () => {
setSearchOpen(s => !s);
setNavOpen(false);
};
return (
<header
id="header"
className={classnames({
docs: isDocs,
blog: isBlog,
scrolled,
})}
return (
<StyledHeader scrolled={scrolled} id="header" hasHeroBelow={hasHeroBelow}>
<Notifications />
<HeaderContainer>
<Logo>
<Link to="/">
<img src={logo} alt="Netlify CMS logo" />
</Link>
</Logo>
<MenuActions>
<SearchBtn onClick={handleSearchBtnClick}>
{isSearchOpen ? <span>&times;</span> : <img src={searchIcon} alt="search" />}
</SearchBtn>
<MenuBtn onClick={handleMenuBtnClick}>
{isNavOpen ? <span>&times;</span> : <span>&#9776;</span>}
</MenuBtn>
</MenuActions>
<SearchBox open={isSearchOpen}>
<DocSearch />
</SearchBox>
<Menu open={isNavOpen}>
<MenuList>
<MenuItem
css={css`
margin-top: 8px;
`}
>
<div className="contained">
<div className="logo-container">
<Link to="/" className="logo">
<img src={logo} alt="Netlify CMS" />
</Link>
<DocSearch />
</div>
<div className="nav-container">
<span className="gh-button">
<GitHubButton />
</span>
<Link className="nav-link docs-link" to="/docs/intro/">
Docs
</Link>
<Link className="nav-link contributing-link" to="/docs/contributor-guide/">
Contributing
</Link>
<Link className="nav-link" to="/community/">
Community
</Link>
<Link className="nav-link" to="/blog/">
Blog
</Link>
</div>
</div>
</header>
);
}}
</Location>
);
}
}
<GitHubButton
href="https://github.com/netlify/netlify-cms"
data-icon="octicon-star"
data-show-count="true"
aria-label="Star netlify/netlify-cms on GitHub"
>
Star
</GitHubButton>
</MenuItem>
<MenuItem>
<NavLink to="/docs/intro/">Docs</NavLink>
</MenuItem>
<MenuItem>
<NavLink to="/docs/contributor-guide/">Contributing</NavLink>
</MenuItem>
<MenuItem>
<NavLink to="/community/">Community</NavLink>
</MenuItem>
<MenuItem>
<NavLink to="/blog/">Blog</NavLink>
</MenuItem>
</MenuList>
</Menu>
</HeaderContainer>
</StyledHeader>
);
};
export default Header;

View File

@ -0,0 +1,16 @@
import styled from '@emotion/styled';
import theme from '../theme';
import { mq } from '../utils';
const HeroTitle = styled.h1`
color: ${theme.colors.green};
font-size: ${theme.fontsize[6]};
margin-bottom: ${theme.space[1]};
${mq[2]} {
font-size: ${theme.fontsize[7]};
margin-bottom: ${theme.space[2]};
}
`;
export default HeroTitle;

View File

@ -0,0 +1,36 @@
import React from 'react';
import styled from '@emotion/styled';
import Container from './container';
import Page from './page';
import theme from '../theme';
const Header = styled.header`
text-align: center;
padding-top: ${theme.space[7]};
padding-bottom: ${theme.space[7]};
`;
const Title = styled.h2`
font-size: ${theme.fontsize[6]};
`;
const Text = styled.div`
max-width: 710px;
margin: 0 auto;
`;
const HomeSection = ({ title, text, children, ...props }) => (
<Page as="section" {...props}>
<Container>
<Header>
<Title>{title}</Title>
{text && <Text>{text}</Text>}
</Header>
{children}
</Container>
</Page>
);
export default HomeSection;

View File

@ -1,67 +1,53 @@
import React, { Fragment } from 'react';
import React from 'react';
import Helmet from 'react-helmet';
import { graphql, StaticQuery } from 'gatsby';
import { ThemeProvider } from 'emotion-theming';
import Header from './header';
import Footer from './footer';
import Notification from './notification';
import GlobalStyles from '../global-styles';
import theme from '../theme';
import '../css/imports/base.css';
import '../css/imports/utilities.css';
import '../css/imports/chat.css';
const Layout = ({ children }) => {
return (
<StaticQuery
query={graphql`
query layoutQuery {
site {
siteMetadata {
title
description
}
}
footer: file(relativePath: { regex: "/global/" }) {
childDataYaml {
footer {
buttons {
url
name
}
}
}
}
notifs: file(relativePath: { regex: "/notifications/" }) {
childDataYaml {
notifications {
published
loud
message
url
}
}
const LAYOUT_QUERY = graphql`
query layoutQuery {
site {
siteMetadata {
title
description
}
}
footer: file(relativePath: { regex: "/global/" }) {
childDataYaml {
footer {
buttons {
url
name
}
}
`}
>
}
}
}
`;
const Layout = ({ hasPageHero, children }) => {
return (
<StaticQuery query={LAYOUT_QUERY}>
{data => {
const { title, description } = data.site.siteMetadata;
const notifs = data.notifs.childDataYaml.notifications.filter(notif => notif.published);
return (
<Fragment>
<ThemeProvider theme={theme}>
<GlobalStyles />
<Helmet defaultTitle={title} titleTemplate={`%s | ${title}`}>
<meta name="description" content={description} />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:400,100,100italic,300,300italic,400italic,500,700,900|Roboto+Mono:400,700"
/>
</Helmet>
{notifs.map((node, i) => (
<Notification key={i} url={node.url} loud={node.loud}>
{node.message}
</Notification>
))}
<Header notifications={notifs} />
<Header hasHeroBelow={hasPageHero} />
{children}
<Footer buttons={data.footer.childDataYaml.footer.buttons} />
</Fragment>
</ThemeProvider>
);
}}
</StaticQuery>

View File

@ -0,0 +1,10 @@
import styled from '@emotion/styled';
const Lead = styled.p`
font-size: 20px;
margin-bottom: 24px;
${p => p.light && 'color: white;'};
`;
export default Lead;

View File

@ -0,0 +1,132 @@
import React from 'react';
import styled from '@emotion/styled';
import theme from '../theme';
const StyledMarkdown = styled.div`
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: ${theme.lineHeight[1]};
margin-top: 2em;
margin-bottom: 0.25em;
}
h1 {
font-size: ${theme.fontsize[6]};
}
h2 {
font-size: ${theme.fontsize[5]};
}
h3 {
font-size: ${theme.fontsize[4]};
}
h4 {
font-size: ${theme.fontsize[3]};
}
ol,
ul {
margin-left: ${theme.space[3]};
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
li {
margin-bottom: 0;
}
ol,
ul,
p {
font-size: 18px;
margin-bottom: 1rem;
}
a {
font-weight: bold;
&:hover {
text-decoration: underline;
}
}
table {
border: 0;
background: #f7f7f7;
border-radius: 4px;
margin-top: 40px;
margin-bottom: 40px;
width: 100%;
text-align: left;
}
tbody tr {
&:nth-child(odd) {
background: #fdfdfd;
}
}
th,
td {
padding: 8px;
}
th {
font-weight: 700;
font-size: 18px;
}
td {
font-size: 14px;
}
iframe {
width: 100%;
}
pre {
border-radius: ${theme.radii[2]};
margin-bottom: ${theme.space[4]};
margin-top: ${theme.space[4]};
}
pre > code {
font-size: ${theme.fontsize[2]};
line-height: ${theme.lineHeight[0]};
}
*:not(pre) > code {
color: inherit;
background: #e6e6e6;
border-radius: 2px;
padding: 2px 6px;
white-space: nowrap;
font-size: ${theme.fontsize[2]};
}
`;
const Markdown = ({ html }) => {
return <StyledMarkdown dangerouslySetInnerHTML={{ __html: html }} />;
};
export default Markdown;

View File

@ -0,0 +1,10 @@
import styled from '@emotion/styled';
import theme from '../theme';
const MetaInfo = styled.p`
font-size: ${theme.fontsize[2]};
margin-bottom: ${theme.space[4]};
`;
export default MetaInfo;

View File

@ -1,52 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'gatsby';
class MobileNav extends Component {
state = {
isOpen: false,
};
toggleNav = () => {
this.setState({
isOpen: !this.state.isOpen,
});
};
render() {
const { items } = this.props;
const { isOpen } = this.state;
return (
<div className="mobile-docs-nav">
<button className="btn-primary mobile-docs-nav-btn" onClick={this.toggleNav}>
{isOpen ? <span>&times;</span> : <span>&#9776;</span>} {isOpen ? 'Hide' : 'Show'}{' '}
Navigation
</button>
{isOpen && (
<nav className="mobile-docs-nav-content">
<ul className="mobile-docs-nav-list">
{items.map(item => (
<li key={item.title} className="mobile-docs-nav-item">
{item.title}
<ul className="mobile-docs-nav-list">
{item.group.edges.map(({ node }) => (
<li key={node.fields.slug} className="mobile-docs-nav-item">
<Link
to={node.fields.slug}
className="mobile-docs-nav-link"
onClick={this.toggleNav}
>
{node.frontmatter.title}
</Link>
</li>
))}
</ul>
</li>
))}
</ul>
</nav>
)}
</div>
);
}
}
export default MobileNav;

View File

@ -1,10 +1,46 @@
import React from 'react';
import cn from 'classnames';
import styled from '@emotion/styled';
import { css } from '@emotion/core';
import theme from '../theme';
const Notif = styled.a`
background-color: ${theme.colors.darkerGray};
color: white;
display: block;
padding: ${theme.space[2]} ${theme.space[3]};
text-align: center;
/* prettier-ignore */
${p =>
p.loud &&
css`
background-color: ${theme.colors.green};
color: ${theme.colors.darkerGray};
`}
em {
font-style: normal;
color: #8b8b8b;
padding: 0 8px;
}
sup,
sub {
font-size: initial;
vertical-align: initial;
}
.text-link {
text-decoration: underline;
color: ${theme.colors.green};
}
`;
const Notification = ({ url, loud, children }) => (
<a href={url} className={cn('notification', { 'notification-loud': loud })}>
<Notif href={url} loud={loud}>
{children}
</a>
</Notif>
);
export default Notification;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { graphql, StaticQuery } from 'gatsby';
import Notification from './notification';
const NOTIFS_QUERY = graphql`
query notifs {
file(relativePath: { regex: "/notifications/" }) {
childDataYaml {
notifications {
published
loud
message
url
}
}
}
}
`;
const Notifications = () => (
<StaticQuery query={NOTIFS_QUERY}>
{data => {
const notifs = data.file.childDataYaml.notifications.filter(notif => notif.published);
return notifs.map((node, i) => (
<Notification key={i} url={node.url} loud={node.loud}>
{node.message}
</Notification>
));
}}
</StaticQuery>
);
export default Notifications;

View File

@ -0,0 +1,29 @@
import React from 'react';
import { css } from '@emotion/core';
import Container from './container';
import theme from '../theme';
import { mq } from '../utils';
const PageHero = ({ children }) => (
<section
css={css`
background: ${theme.colors.darkerGray};
background-image: linear-gradient(to bottom, #2a2c24 0%, ${theme.colors.darkerGray} 20%);
color: ${theme.colors.blueGray};
position: relative;
padding-top: ${theme.space[6]};
padding-bottom: ${theme.space[6]};
${mq[3]} {
padding-top: ${theme.space[6]};
padding-bottom: ${theme.space[8]};
}
`}
>
<Container>{children}</Container>
</section>
);
export default PageHero;

View File

@ -0,0 +1,16 @@
import styled from '@emotion/styled';
import theme from '../theme';
import { mq } from '../utils';
const Page = styled.div`
padding-top: ${theme.space[5]};
padding-bottom: ${theme.space[5]};
${mq[1]} {
padding-top: ${theme.space[6]};
padding-bottom: ${theme.space[6]};
}
`;
export default Page;

View File

@ -1,23 +1,67 @@
import React from 'react';
import moment from 'moment';
import styled from '@emotion/styled';
import { css } from '@emotion/core';
import Markdownify from '../components/markdownify';
import theme from '../theme';
const ReleaseLink = styled.a`
color: white;
display: block;
padding: ${theme.space[2]} ${theme.space[3]};
border-radius: ${theme.radii[1]};
height: 100%;
&:hover {
background: ${theme.colors.darkGray};
}
`;
const Version = styled.span`
background: ${theme.colors.shadeBlue};
font-size: ${theme.fontsize[1]};
padding: 0 ${theme.space[1]};
border-radius: ${theme.radii[1]};
font-weight: 700;
margin-right: ${theme.space[2]};
color: ${theme.colors.gray};
`;
const Release = ({ version, versionPrevious, date, description, url }) => {
const displayDate = moment(date).format('MMMM D, YYYY');
const defaultUrl = `https://github.com/netlify/netlify-cms/compare/netlify-cms@${versionPrevious}...netlify-cms@${version}`;
return (
<a href={url || defaultUrl} key={version}>
<li>
<div className="update-metadata">
<span className="update-version">{version}</span>
<span className="update-date">{displayDate}</span>
<li
css={css`
flex: 1;
`}
>
<ReleaseLink href={url || defaultUrl}>
<div
css={css`
margin-bottom: ${theme.space[1]};
`}
>
<Version>{version}</Version>
<span
css={css`
font-size: ${theme.fontsize[1]};
color: rgba(255, 255, 255, 0.6);
`}
>
{displayDate}
</span>
</div>
<span className="update-description">
<span
css={css`
font-size: ${theme.fontsize[2]};
`}
>
<Markdownify source={description} />
</span>
</li>
</a>
</ReleaseLink>
</li>
);
};

View File

@ -0,0 +1,23 @@
import styled from '@emotion/styled';
import theme from '../theme';
const SectionLabel = styled.h3`
color: ${theme.colors.gray};
font-size: ${theme.fontsize[1]};
font-weight: 600;
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: ${theme.space[4]};
&:after {
background: ${theme.colors.darkGreen};
content: ' ';
display: block;
height: 2px;
margin-top: 5px;
width: ${theme.space[5]};
}
`;
export default SectionLabel;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { css } from '@emotion/core';
import Page from './page';
import { mq } from '../utils';
const SidebarLayout = ({ sidebar, children }) => (
<Page
css={css`
${mq[1]} {
display: grid;
grid-template-columns: 300px 1fr;
grid-gap: 2rem;
}
`}
>
<div>{sidebar}</div>
<div>{children}</div>
</Page>
);
export default SidebarLayout;

View File

@ -0,0 +1,59 @@
import React, { useState, useEffect } from 'react';
import styled from '@emotion/styled';
import theme from '../theme';
const TocList = styled.ol`
margin: ${theme.space[2]} 0;
padding-left: ${theme.space[3]};
border-left: 2px solid ${theme.colors.lightestGray};
list-style-type: none;
`;
const TocLink = styled.a`
display: block;
font-size: ${theme.fontsize[2]};
color: ${theme.colors.gray};
transition: color 0.2s;
line-height: ${theme.lineHeight[1]};
margin: ${theme.space[2]} 0;
&:hover {
color: ${theme.colors.darkGreen};
}
`;
/**
* Maually get table of contents since tableOfContents from markdown
* nodes have code added.
*
* https://github.com/gatsbyjs/gatsby/issues/5436
*/
const TableOfContents = () => {
const [headings, setHeadings] = useState([]);
useEffect(() => {
const contentHeadings = document.querySelectorAll('[data-docs-content] h2');
const headings = [];
contentHeadings.forEach(h => {
headings.push({
id: h.id,
text: h.innerText,
});
});
setHeadings(headings);
}, []);
return (
<TocList>
{headings.map(h => (
<li key={h.id}>
<TocLink href={`#${h.id}`}>{h.text}</TocLink>
</li>
))}
</TocList>
);
};
export default TableOfContents;

View File

@ -1,7 +1,65 @@
import React, { Component } from 'react';
import React, { useState } from 'react';
import styled from '@emotion/styled';
import theme from '../theme';
import screenshotEditor from '../img/screenshot-editor.jpg';
const VideoLink = styled.a`
position: relative;
cursor: pointer;
display: block;
&:hover {
div {
background-color: ${theme.colors.blue};
box-shadow: 0 6px 18px 0 rgba(0, 0, 0, 0.15), 0 2px 6px 0 rgba(0, 0, 0, 0.3);
transform: scale(1.1);
}
svg {
fill: #fff;
}
}
&:active {
div {
transform: scale(0.9);
}
}
img,
iframe {
width: 100%;
border-radius: ${theme.radii[2]};
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.15), 0 3px 9px 0 rgba(0, 0, 0, 0.3);
}
`;
const VideoButton = styled.div`
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 90px;
height: 90px;
margin: auto;
color: ${theme.colors.blue};
background-color: rgba(255, 255, 255, 0.85);
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.15);
border-radius: 100px;
transition: 0.1s;
svg {
position: absolute;
left: 30px;
top: 24px;
width: 44px;
height: 44px;
fill: #3a69c7;
}
`;
/**
* We should be able to import complete inline svg's rather than base64, this
* component is a stopgap for now. Source in '../img/play.svg'.
@ -16,45 +74,35 @@ const PlayIcon = ({ className }) => (
</svg>
);
class VideoEmbed extends Component {
state = {
toggled: false,
};
toggleVideo = () => {
this.setState({
toggled: true,
});
};
render() {
const { toggled } = this.state;
const VideoEmbed = () => {
const [toggled, setToggled] = useState(false);
const embedcode = (
<iframe
width={560}
height={315}
src="https://www.youtube-nocookie.com/embed/p6h-rYSVX90?rel=0&showinfo=0&autoplay=1"
frameBorder={0}
allow="autoplay; encrypted-media"
allowFullScreen
title="video_embed"
/>
);
const toggleVideo = () => setToggled(true);
const imgPlaceholder = (
<img src={screenshotEditor} className="responsive" alt="editor video screenshot" />
);
const embedcode = (
<iframe
title="Netlify CMS video"
width={560}
height={315}
src="https://www.youtube-nocookie.com/embed/p6h-rYSVX90?rel=0&showinfo=0&autoplay=1"
frameBorder={0}
allow="autoplay; encrypted-media"
allowFullScreen
/>
);
return (
<div className="hero-graphic" onClick={this.toggleVideo}>
{toggled ? embedcode : imgPlaceholder}
{!toggled && (
<div className="hero-videolink">
<PlayIcon className="hero-videolink-arrow" />
</div>
)}
</div>
);
}
}
const imgPlaceholder = <img src={screenshotEditor} alt="Netlify CMS editor" />;
return (
<VideoLink onClick={toggleVideo}>
{toggled ? embedcode : imgPlaceholder}
{!toggled && (
<VideoButton>
<PlayIcon />
</VideoButton>
)}
</VideoLink>
);
};
export default VideoEmbed;

View File

@ -1,10 +1,26 @@
import React from 'react';
import { css } from '@emotion/core';
const WhatsNew = ({ children }) => (
<section className="whatsnew">
<div className="contained">
<ol>{children}</ol>
</div>
import Container from './container';
import Release from './release';
import Grid from './grid';
import theme from '../theme';
const WhatsNew = ({ updates }) => (
<section
css={css`
background: ${theme.colors.lightishGray};
padding-top: ${theme.space[6]};
padding-bottom: ${theme.space[5]};
`}
>
<Container>
<Grid as="ol" cols={3}>
{updates.slice(0, 3).map((item, idx) => (
<Release {...item} versionPrevious={updates[idx + 1].version} key={item.version} />
))}
</Grid>
</Container>
</section>
);

View File

@ -1,11 +1,18 @@
import React from 'react';
import classnames from 'classnames';
const WidgetDoc = ({ visible, label, body, html }) => (
<div className={classnames('widget', { widget_open: visible })}>
<h3>{label}</h3>
{body ? body : <div dangerouslySetInnerHTML={{ __html: html }} />}
</div>
);
import Markdown from './markdown';
const WidgetDoc = ({ visible, label, body, html }) => {
if (!visible) {
return null;
}
return (
<div>
<h3>{label}</h3>
<Markdown html={body || html} />
</div>
);
};
export default WidgetDoc;

View File

@ -1,79 +1,76 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import React, { useState, useEffect } from 'react';
import styled from '@emotion/styled';
import WidgetDoc from './widget-doc';
import Button from './button';
import '../css/imports/widgets.css';
import theme from '../theme';
class Widgets extends Component {
state = {
currentWidget: null,
};
const WidgetsNav = styled.nav`
margin-bottom: 1rem;
componentDidMount() {
const { widgets } = this.props;
> button {
margin-right: 8px;
margin-bottom: 8px;
}
`;
const WidgetsContent = styled.div`
background: ${theme.colors.lightGray};
padding: ${theme.space[3]};
border-radius: 4px;
`;
const Widgets = ({ widgets }) => {
const [currentWidget, setWidget] = useState(null);
useEffect(() => {
const hash = window.location.hash ? window.location.hash.replace('#', '') : '';
const widgetsContainHash = widgets.edges.some(w => w.node.frontmatter.title === hash);
if (widgetsContainHash) {
return this.setState({
currentWidget: hash,
});
return setWidget(hash);
}
this.setState({
currentWidget: widgets.edges[0].node.frontmatter.title,
});
}
setWidget(widgets.edges[0].node.frontmatter.title);
}, []);
handleWidgetChange = (event, title) => {
const handleWidgetChange = (event, title) => {
event.preventDefault();
this.setState(
{
currentWidget: title,
},
() => {
window.history.pushState(null, null, `#${title}`);
},
);
setWidget(title);
window.history.pushState(null, null, `#${title}`);
};
render() {
const { widgets } = this.props;
const { currentWidget } = this.state;
return (
<div>
<section className="widgets">
<div className="widgets__cloud">
{widgets.edges.map(({ node }) => {
const { label, title } = node.frontmatter;
return (
<button
key={title}
className={classnames('widgets__item', {
widgets__item_active: currentWidget === title,
})}
onClick={event => this.handleWidgetChange(event, title)}
>
{label}
</button>
);
})}
</div>
<div className="widgets__container">
{widgets.edges.map(({ node }) => {
const { frontmatter, html } = node;
const { title, label } = frontmatter;
const isVisible = currentWidget === title;
return <WidgetDoc key={label} visible={isVisible} label={label} html={html} />;
})}
</div>
</section>
</div>
);
}
}
return (
<section>
<WidgetsNav>
{widgets.edges.map(({ node }) => {
const { label, title } = node.frontmatter;
return (
<Button
key={title}
active={currentWidget === title}
onClick={event => handleWidgetChange(event, title)}
outline
>
{label}
</Button>
);
})}
</WidgetsNav>
<WidgetsContent>
{widgets.edges.map(({ node }) => {
const { frontmatter, html } = node;
const { title, label } = frontmatter;
const isVisible = currentWidget === title;
return <WidgetDoc key={label} visible={isVisible} label={label} html={html} />;
})}
</WidgetsContent>
</section>
);
};
export default Widgets;