docs: convert website from postcss to emotion (#2068)
This commit is contained in:
committed by
Shawn Erquhart
parent
7b0838dfef
commit
3d474b1944
34
website/src/components/button.js
Normal file
34
website/src/components/button.js
Normal 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;
|
18
website/src/components/chat-button.js
Normal file
18
website/src/components/chat-button.js
Normal 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;
|
56
website/src/components/community-channels-list.js
Normal file
56
website/src/components/community-channels-list.js
Normal 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;
|
32
website/src/components/container.js
Normal file
32
website/src/components/container.js
Normal 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;
|
@ -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>×</span> : <span>☰</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 };
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
126
website/src/components/event-box.js
Normal file
126
website/src/components/event-box.js
Normal 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>…</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;
|
@ -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;
|
43
website/src/components/features.js
Normal file
43
website/src/components/features.js
Normal 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;
|
@ -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;
|
||||
|
14
website/src/components/grid.js
Normal file
14
website/src/components/grid.js
Normal 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;
|
@ -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>×</span> : <img src={searchIcon} alt="search" />}
|
||||
</SearchBtn>
|
||||
<MenuBtn onClick={handleMenuBtnClick}>
|
||||
{isNavOpen ? <span>×</span> : <span>☰</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;
|
||||
|
16
website/src/components/hero-title.js
Normal file
16
website/src/components/hero-title.js
Normal 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;
|
36
website/src/components/home-section.js
Normal file
36
website/src/components/home-section.js
Normal 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;
|
@ -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>
|
||||
|
10
website/src/components/lead.js
Normal file
10
website/src/components/lead.js
Normal 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;
|
132
website/src/components/markdown.js
Normal file
132
website/src/components/markdown.js
Normal 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;
|
10
website/src/components/meta-info.js
Normal file
10
website/src/components/meta-info.js
Normal 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;
|
@ -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>×</span> : <span>☰</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;
|
@ -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;
|
||||
|
34
website/src/components/notifications.js
Normal file
34
website/src/components/notifications.js
Normal 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;
|
29
website/src/components/page-hero.js
Normal file
29
website/src/components/page-hero.js
Normal 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;
|
16
website/src/components/page.js
Normal file
16
website/src/components/page.js
Normal 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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
23
website/src/components/section-label.js
Normal file
23
website/src/components/section-label.js
Normal 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;
|
22
website/src/components/sidebar-layout.js
Normal file
22
website/src/components/sidebar-layout.js
Normal 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;
|
59
website/src/components/table-of-contents.js
Normal file
59
website/src/components/table-of-contents.js
Normal 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;
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user