diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 00000000..7a5e97ee --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,16 @@ +name: Website Publish + +on: + push: + branches: + - dev + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: enriikke/gatsby-gh-pages-action@v2 + with: + access-token: ${{ secrets.ACCESS_TOKEN }} + working-dir: 'website' \ No newline at end of file diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 48ae1127..00000000 --- a/netlify.toml +++ /dev/null @@ -1,7 +0,0 @@ -[build] - base = "/" - ignore = "git diff --quiet HEAD^ HEAD -- . ':!website/'" - - [build.environment] - YARN_FLAGS = "--frozen-lockfile" - YARN_VERSION = "1.22.4" diff --git a/simple-cms-icon.png b/simple-cms-icon.png new file mode 100644 index 00000000..89a1e32b Binary files /dev/null and b/simple-cms-icon.png differ diff --git a/website/.babelrc b/website/.babelrc new file mode 100644 index 00000000..131352d3 --- /dev/null +++ b/website/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": ["babel-preset-gatsby"], + "plugins": [ + [ + "prismjs", + { + "languages": ["javascript", "css", "markup", "yaml", "json"], + "plugins": ["line-numbers"], + "theme": "tomorrow", + "css": true + } + ] + ] +} diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..0174e7b8 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,3 @@ +.cache +public +dist diff --git a/website/.markdownlint.json b/website/.markdownlint.json new file mode 100644 index 00000000..77811aa6 --- /dev/null +++ b/website/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "default": false, + "MD032": true +} diff --git a/website/.nvmrc b/website/.nvmrc new file mode 100644 index 00000000..cbe86a23 --- /dev/null +++ b/website/.nvmrc @@ -0,0 +1 @@ +--lts diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..d7a58a12 --- /dev/null +++ b/website/README.md @@ -0,0 +1,19 @@ +# Simple CMS Website & Docs + +This directory builds netlifycms.org. If you'd like to propose changes to the site or docs, you'll find the source files in here. + +## Local development + +The site is built with [GatsbyJS](https://gatsbyjs.org/). + +To run the site locally, you'll need to have [Node](https://nodejs.org) and [Yarn](https://yarnpkg.com/en/) installed on your computer. + +From your terminal window, `cd` into the `website` directory of the repo, and run + +```bash +yarn +yarn start +``` + +Then visit http://localhost:8000/ - Gatsby will automatically reload CSS or +refresh the page when stylesheets or content changes. diff --git a/website/data/.keep b/website/data/.keep new file mode 100644 index 00000000..e69de29b diff --git a/website/data/docs.yml b/website/data/docs.yml new file mode 100644 index 00000000..168f6f02 --- /dev/null +++ b/website/data/docs.yml @@ -0,0 +1,11 @@ +styleoverrides: '/docs.css' +headline: Netlify builds, deploys, and hosts your front end. +bottomcta: + hook: Want to get started quick? + btns: + - type: primary + btntext: View our Templates + linksto: https://app.netlify.com/signup/templates + - type: secondary + btntext: Read our Tutorials + linksto: /tags/tutorial/ diff --git a/website/data/global.yaml b/website/data/global.yaml new file mode 100644 index 00000000..36380cd1 --- /dev/null +++ b/website/data/global.yaml @@ -0,0 +1,6 @@ +footer: + buttons: + - name: "Twitter" + url: "https://twitter.com/SimpleCMSOrg" + - name: "GitHub" + url: "https://github.com/SimpleCMS/simple-cms" diff --git a/website/data/landing.yaml b/website/data/landing.yaml new file mode 100644 index 00000000..4ba2e393 --- /dev/null +++ b/website/data/landing.yaml @@ -0,0 +1,41 @@ +hero: + headline: "Open source content management for your Git workflow" + subhead: "Use Simple CMS with any static site generator for a faster and more flexible web project" + devfeatures: + - feature: "Static + content management = ♥" + description: "Get the speed, security, and scalability of a static site, while still providing a convenient editing interface for content." + - feature: "An integrated part of your Git workflow" + description: "Content is stored in your Git repository alongside your code for easier versioning, multi-channel publishing, and the option to handle content updates directly in Git." + - feature: "An extensible CMS built on React" + description: "Simple CMS is built as a single-page React app. Create custom-styled previews, UI widgets, and editor plugins or add backends to support different Git platform APIs." + +cta: + primaryhook: "Getting started is simple and free." + primary: "Choose a template that’s pre-configured with a static site generator and deploys to a global CDN in one click." + button: "[Get started](/docs/start-with-a-template/)" + +editors: + hook: "A CMS that developers and content editors can agree on" + intro: "You get to implement modern front end tools to deliver a faster, safer, and more scalable site. Editors get a friendly UI and intuitive workflow that meets their content management requirements." + features: + - feature: "Editor-friendly user interface" + description: "The web-based app includes rich-text editing, real-time preview, and drag-and-drop media uploads." + imgpath: "feature-editor.svg" + - feature: "Intuitive workflow for content teams" + description: "Writers and editors can easily manage content from draft to review to publish across any number of custom content types." + imgpath: "feature-workflow.svg" + - feature: "Instant access without GitHub account" + description: "With [Git Gateway](/docs/git-gateway-backend/#git-gateway-with-netlify-identity), you can add CMS access for any team member — even if they don’t have a GitHub account. " + imgpath: "feature-access.svg" + +community: + hook: "Supported by a growing community" + features: + - feature: "Built on the Jamstack" + description: "Simple CMS is based on client-side JavaScript, reusable APIs and prebuilt Markup. Compared to server-side CMS like WordPress, this means better performance, higher security, lower cost of scaling, and a better developer experience. You can learn more about the Jamstack on [jamstack.org](https://jamstack.org)." + - feature: "Support when you need it" + description: "Get up and running with comprehensive [documentation](/docs) and templates or work through difficult problems in our [community chat](https://netlifycms.org/chat)." + - feature: "A community-driven project you can help evolve" + description: "Simple CMS is built by a community of more than 100 contributors — and you can help. Read the [contributing guide](/docs/contributor-guide) to join in." + contributors: "Made possible by awesome contributors" + diff --git a/website/data/notifications.yml b/website/data/notifications.yml new file mode 100644 index 00000000..2e537a9a --- /dev/null +++ b/website/data/notifications.yml @@ -0,0 +1,7 @@ +notifications: + - loud: true + message: We have a community chat - join now to ask questions and discuss the + project with other devs! + published: false + title: Chat shoutout + url: https://netlifycms.org/chat diff --git a/website/data/updates.yml b/website/data/updates.yml new file mode 100644 index 00000000..651246e0 --- /dev/null +++ b/website/data/updates.yml @@ -0,0 +1,5 @@ +updates: + - date: 2022-10-30T00:00:00.000Z + version: 1.0.0 + description: The first major release of Simple CMS with an all-new UI, revamped + documentation and much more. diff --git a/website/gatsby-browser.js b/website/gatsby-browser.js new file mode 100644 index 00000000..54c19635 --- /dev/null +++ b/website/gatsby-browser.js @@ -0,0 +1,9 @@ +// Make scroll behavior of internal links smooth +exports.onClientEntry = () => { + const SmoothScroll = require('smooth-scroll'); + new SmoothScroll('a[href*="#"]', { + offset() { + return document.querySelector('#header').offsetHeight; + }, + }); +}; diff --git a/website/gatsby-config.js b/website/gatsby-config.js new file mode 100644 index 00000000..29ce61c8 --- /dev/null +++ b/website/gatsby-config.js @@ -0,0 +1,76 @@ +const fs = require('fs'); +const yaml = require('js-yaml'); + +const pkg = require('./package.json'); + +const staticConfig = yaml.load(fs.readFileSync('./site.yml', 'utf8')); + +module.exports = { + siteMetadata: { + title: 'Simple CMS | Open-Source Content Management System', + description: 'Open source content management for your Git workflow', + siteUrl: pkg.homepage, + menu: staticConfig.menu, + }, + plugins: [ + { + resolve: 'gatsby-plugin-manifest', + options: { + name: 'SimpleCMS', + short_name: 'SimpleCMS', + start_url: '/', + background_color: '#ffffff', + theme_color: '#ffffff', + display: 'standalone', + icon: 'static/img/favicon/icon-512x512.png', + }, + }, + { + resolve: 'gatsby-source-filesystem', + options: { + path: `${__dirname}/content`, + name: 'content', + }, + }, + { + resolve: 'gatsby-source-filesystem', + options: { + path: `${__dirname}/data`, + name: 'data', + }, + }, + { + resolve: 'gatsby-transformer-remark', + options: { + // prettier-ignore + plugins: [ + 'gatsby-remark-autolink-headers', + { + resolve: "gatsby-remark-external-links", + options: { + target: "_blank", + rel: "noopener noreferrer" + } + }, + { + resolve: 'gatsby-remark-prismjs', + options: { + noInlineHighlight: true, + }, + }, + ], + }, + }, + 'gatsby-transformer-yaml', + 'gatsby-transformer-json', + 'gatsby-plugin-emotion', + 'gatsby-plugin-react-helmet', + 'gatsby-plugin-catch-links', + { + resolve: 'gatsby-plugin-netlify-cms', + options: { + modulePath: `${__dirname}/src/cms/cms.js`, + }, + }, + ], +}; diff --git a/website/gatsby-node.js b/website/gatsby-node.js new file mode 100644 index 00000000..0369cb1a --- /dev/null +++ b/website/gatsby-node.js @@ -0,0 +1,104 @@ +const path = require('path'); +const { createFilePath } = require('gatsby-source-filesystem'); + +exports.createPages = async ({ graphql, actions }) => { + const { createPage } = actions; + + const docPage = path.resolve('./src/templates/doc-page.js'); + const blogPost = path.resolve('./src/templates/blog-post.js'); + + // get all markdown with a frontmatter path field and title + const allMarkdown = await graphql(` + { + allMarkdownRemark(filter: { frontmatter: { title: { ne: null } } }) { + edges { + node { + fields { + slug + } + frontmatter { + title + } + } + } + } + } + `); + + if (allMarkdown.errors) { + console.error(allMarkdown.errors); // eslint-disable-line no-console + throw Error(allMarkdown.errors); + } + + allMarkdown.data.allMarkdownRemark.edges.forEach(({ node }) => { + const { slug } = node.fields; + + let template = docPage; + + if (slug.includes('blog/')) { + template = blogPost; + } + + createPage({ + path: slug, + component: template, + context: { + slug, + }, + }); + }); +}; + +function pad(n) { + return n >= 10 ? n : `0${n}`; +} + +exports.onCreateNode = ({ node, actions, getNode }) => { + const { createNodeField } = actions; + + if (node.internal.type === 'MarkdownRemark') { + const value = createFilePath({ node, getNode }); + const { relativePath } = getNode(node.parent); + + let slug = value; + + if (relativePath.includes('blog/')) { + const date = new Date(node.frontmatter.date); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const filename = path.basename(relativePath, '.md'); + slug = `/blog/${year}/${pad(month)}/${filename}`; + + createNodeField({ + node, + name: 'date', + value: date.toJSON(), + }); + } + + // used for doc posts + createNodeField({ + node, + name: 'slug', + value: slug, + }); + + // used to create GitHub edit link + createNodeField({ + node, + name: 'path', + value: relativePath, + }); + } +}; + +exports.onCreateWebpackConfig = ({ actions }) => { + actions.setWebpackConfig({ + resolve: { + extensions: ['.ts', '.tsx', '.js', '.json'], + alias: { + moment$: 'moment/moment.js', + }, + }, + }); +}; diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..a19c832a --- /dev/null +++ b/website/package.json @@ -0,0 +1,53 @@ +{ + "name": "simple-cms-site", + "version": "1.0.0", + "description": "Simple CMS documentation website built with Gatsby", + "scripts": { + "start": "gatsby develop", + "build": "gatsby build && rm -rf dist && mv public dist", + "lint": "markdownlint 'content/docs/**/*.md'", + "reset": "rm -rf .cache" + }, + "author": "", + "homepage": "https://www.netlifycms.org/", + "license": "MIT", + "dependencies": { + "@emotion/cache": "^10.0.29", + "@emotion/core": "^10.0.35", + "@emotion/styled": "^10.0.27", + "dayjs": "^1.8.23", + "emotion-theming": "^10.0.27", + "gatsby": "3.14.6", + "gatsby-plugin-catch-links": "3.14.0", + "gatsby-plugin-emotion": "^4.0.0", + "gatsby-plugin-manifest": "3.14.0", + "gatsby-plugin-netlify-cms": "5.14.0", + "gatsby-plugin-react-helmet": "4.14.0", + "gatsby-remark-autolink-headers": "4.11.0", + "gatsby-remark-external-links": "^0.0.4", + "gatsby-remark-prismjs": "5.11.0", + "gatsby-source-filesystem": "3.14.0", + "gatsby-transformer-json": "3.14.0", + "gatsby-transformer-remark": "4.11.0", + "gatsby-transformer-yaml": "3.14.0", + "js-yaml": "^4.0.0", + "lodash": "^4.17.15", + "moment": "^2.24.0", + "netlify-cms-app": "^2.15.72", + "prismjs": "^1.21.0", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-github-btn": "^1.1.1", + "react-helmet": "^6.0.0", + "react-markdown": "^6.0.0", + "smooth-scroll": "^16.1.2" + }, + "devDependencies": { + "babel-plugin-prismjs": "^2.0.1", + "babel-preset-gatsby": "^1.0.0", + "eslint": "^7.4.0", + "eslint-plugin-import": "^2.20.1", + "markdownlint-cli": "^0.31.0" + }, + "private": true +} diff --git a/website/site.yml b/website/site.yml new file mode 100644 index 00000000..712df383 --- /dev/null +++ b/website/site.yml @@ -0,0 +1,22 @@ +menu: + docs: + - name: Intro + title: Intro to Simple CMS + - name: Accounts + title: Account Settings + - name: Configuration + title: Configuring your Site + - name: Media + title: Media + - name: Workflow + title: Workflow + - name: Collections + title: Collections + - name: Fields + title: Fields + - name: Guides + title: Platform Guides + - name: Customization + title: Customizing Simple CMS + - name: Contributing + title: Community diff --git a/website/src/cms/cms.js b/website/src/cms/cms.js new file mode 100644 index 00000000..8ab45069 --- /dev/null +++ b/website/src/cms/cms.js @@ -0,0 +1,140 @@ +import React from 'react'; +import CMS from 'netlify-cms-app'; +import dayjs from 'dayjs'; +import Prism from 'prismjs'; +import { CacheProvider } from '@emotion/core'; +import createCache from '@emotion/cache'; + +import BlogPostTemplate from '../components/blog-post-template'; +import { LayoutTemplate as Layout } from '../components/layout'; +import DocsTemplate from '../components/docs-template'; +import WidgetDoc from '../components/widget-doc'; +import WhatsNew from '../components/whats-new'; +import Notification from '../components/notification'; +import Community from '../components/community'; +import siteConfig from '../../site.yml'; + +let emotionCache; +function getEmotionCache() { + const previewPaneIframe = document.querySelector('iframe[class*="PreviewPaneFrame"]'); + const previewPaneHeadEl = previewPaneIframe.contentWindow.document.querySelector('head'); + if (!emotionCache || emotionCache.sheet.container !== previewPaneHeadEl) { + emotionCache = createCache({ container: previewPaneHeadEl }); + } + return emotionCache; +} + +function PreviewContainer({ children, highlight }) { + return ( + + {highlight ? {children} : children} + + ); +} + +class Highlight extends React.Component { + constructor(props) { + super(props); + this.ref = React.createRef(); + } + + highlight() { + setTimeout(() => { + if (this.ref.current) { + Prism.highlightAllUnder(this.ref.current); + } + }); + } + + componentDidMount() { + this.highlight(); + } + + componentDidUpdate() { + this.highlight(); + } + + render() { + return
{this.props.children}
; + } +} + +function BlogPostPreview({ entry, widgetFor }) { + const data = entry.get('data'); + return ( + + + + ); +} + +function CommunityPreview({ entry }) { + const { title, headline, subhead, sections } = entry.get('data').toJS(); + return ( + + + + ); +} + +function DocsPreview({ entry, widgetFor }) { + return ( + + + + ); +} + +function WidgetDocPreview({ entry, widgetFor }) { + return ( + + + + ); +} + +function ReleasePreview({ entry }) { + return ( + + ({ + version: release.get('version'), + date: dayjs(release.get('date')).format('MMMM D, YYYY'), + description: release.get('description'), + })) + .toJS()} + /> + + ); +} + +function NotificationPreview({ entry }) { + return ( + + {entry + .getIn(['data', 'notifications']) + .filter(notif => notif.get('published')) + .map((notif, idx) => ( + + {notif.get('message')} + + ))} + + ); +} + +CMS.registerPreviewTemplate('blog', BlogPostPreview); +siteConfig.menu.docs.forEach(group => { + CMS.registerPreviewTemplate(`docs_${group.name}`, DocsPreview); +}); +CMS.registerPreviewTemplate('widget_docs', WidgetDocPreview); +CMS.registerPreviewTemplate('releases', ReleasePreview); +CMS.registerPreviewTemplate('notifications', NotificationPreview); +CMS.registerPreviewTemplate('community', CommunityPreview); diff --git a/website/src/components/blog-post-template.js b/website/src/components/blog-post-template.js new file mode 100644 index 00000000..cb9a936a --- /dev/null +++ b/website/src/components/blog-post-template.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { css } from '@emotion/core'; + +import Container from './container'; +import Markdown from './markdown'; +import MetaInfo from './meta-info'; +import Page from './page'; + +export default function BlogPostTemplate({ title, author, date, body, html }) { + return ( + + +

+ {title} +

+ + by {author} on {date} + + +
+
+ ); +} diff --git a/website/src/components/button.js b/website/src/components/button.js new file mode 100644 index 00000000..6c15ea21 --- /dev/null +++ b/website/src/components/button.js @@ -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%, #4bc9fa 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; diff --git a/website/src/components/chat-button.js b/website/src/components/chat-button.js new file mode 100644 index 00000000..fd37c775 --- /dev/null +++ b/website/src/components/chat-button.js @@ -0,0 +1,20 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +const ChatLink = styled.a` + z-index: 100; + position: fixed; + bottom: 10px; + right: 10px; + cursor: pointer; +`; + +function ChatButton() { + return ( + + + + ); +} + +export default ChatButton; diff --git a/website/src/components/community-channels-list.js b/website/src/components/community-channels-list.js new file mode 100644 index 00000000..5b8b22b7 --- /dev/null +++ b/website/src/components/community-channels-list.js @@ -0,0 +1,59 @@ +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; + } +`; + +function CommunityChannelsList({ channels }) { + return ( + + {channels.map(({ title, description, url }, idx) => ( +
  • + + {title} +

    {description}

    +
    +
  • + ))} +
    + ); +} + +export default CommunityChannelsList; diff --git a/website/src/components/community.js b/website/src/components/community.js new file mode 100644 index 00000000..c6a0c1c4 --- /dev/null +++ b/website/src/components/community.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { css } from '@emotion/core'; + +import Markdownify from './markdownify'; +import PageHero from './page-hero'; +import HeroTitle from './hero-title'; +import Lead from './lead'; +import Container from './container'; +import SectionLabel from './section-label'; +import Page from './page'; +import Grid from './grid'; +import CommunityChannelsList from './community-channels-list'; +import theme from '../theme'; + +function Community({ headline, subhead, sections }) { + return ( + <> + +
    + + + + + + +
    +
    + + + + +
    + {sections.map(({ title: sectionTitle, channels }, channelIdx) => ( + + {sectionTitle} + + + ))} +
    +
    +
    +
    + + ); +} + +export default Community; diff --git a/website/src/components/container.js b/website/src/components/container.js new file mode 100644 index 00000000..c0664f1e --- /dev/null +++ b/website/src/components/container.js @@ -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; diff --git a/website/src/components/docs-nav.js b/website/src/components/docs-nav.js new file mode 100644 index 00000000..cf2339f9 --- /dev/null +++ b/website/src/components/docs-nav.js @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { Link } from 'gatsby'; +import styled from '@emotion/styled'; + +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}; + } +`; + +function DocsNav({ items, location }) { + const [isMenuOpen, setMenuOpen] = useState(false); + + function toggleMenu() { + setMenuOpen(isOpen => !isOpen); + } + + return ( + + + {isMenuOpen ? × : } {isMenuOpen ? 'Hide' : 'Show'}{' '} + Navigation + + + {items.map(item => ( + + {item.title} + + {item.group.edges.map(({ node }) => ( + + + {node.frontmatter.title} + + {location.pathname === node.fields.slug && } + + ))} + + + ))} + + + ); +} + +export default DocsNav; + +export { NavLink }; diff --git a/website/src/components/docs-template.js b/website/src/components/docs-template.js new file mode 100644 index 00000000..5fb66396 --- /dev/null +++ b/website/src/components/docs-template.js @@ -0,0 +1,42 @@ +import React from 'react'; + +import Container from './container'; +import SidebarLayout from './sidebar-layout'; +import EditLink from './edit-link'; +import Widgets from './widgets'; +import Markdown from './markdown'; +import DocsNav from './docs-nav'; + +function DocsSidebar({ docsNav, location }) { + return ( + + ); +} + +export default function DocsTemplate({ + title, + filename, + body, + html, + showWidgets, + widgets, + showSidebar, + docsNav, + location, + group, +}) { + return ( + + }> +
    + {filename && } +

    {title}

    + + {showWidgets && } +
    +
    +
    + ); +} diff --git a/website/src/components/docsearch.js b/website/src/components/docsearch.js new file mode 100644 index 00000000..9d5aa158 --- /dev/null +++ b/website/src/components/docsearch.js @@ -0,0 +1,56 @@ +import React, { useState, useEffect, memo } from 'react'; +import styled from '@emotion/styled'; + +import theme from '../theme'; +import searchIcon from '../img/search.svg'; + +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; +`; + +function DocSearch() { + const [enabled, setEnabled] = useState(true); + + useEffect(() => { + if (window.docsearch) { + window.docsearch({ + apiKey: '08d03dc80862e84c70c5a1e769b13019', + indexName: 'simplecms', + inputSelector: '#algolia-search', + debug: false, // Set debug to true if you want to inspect the dropdown + }); + } else { + setEnabled(false); + } + }, []); + + if (!enabled) { + return null; + } + + return ( + + + + ); +} + +export default memo(DocSearch); diff --git a/website/src/components/edit-link.js b/website/src/components/edit-link.js new file mode 100644 index 00000000..7711ed83 --- /dev/null +++ b/website/src/components/edit-link.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { css } from '@emotion/core'; + +function EditLink({ collection, filename }) { + return ( +
    + + + + {' '} + Edit this page + +
    + ); +} + +export default EditLink; diff --git a/website/src/components/event-box.js b/website/src/components/event-box.js new file mode 100644 index 00000000..24b1abc3 --- /dev/null +++ b/website/src/components/event-box.js @@ -0,0 +1,125 @@ +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``; + +function 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 = ; + + return ( + + {title} + + {loading ? 'loading' : month} + {loading ? ellip : day} + + + {loading ? ( + ellip + ) : ( + + {datePrefix} at {dateSuffix} PT + + )} + + + + + + ); +} + +export default EventBox; diff --git a/website/src/components/features.js b/website/src/components/features.js new file mode 100644 index 00000000..c432f4d8 --- /dev/null +++ b/website/src/components/features.js @@ -0,0 +1,46 @@ +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; + } +`; + +function FeatureItem({ feature, description, imgpath, kind }) { + return ( + + {imgpath && } + + <Markdownify source={feature} /> + + + + + + ); +} + +function Features({ items, kind }) { + return items.map(item => ); +} + +export default Features; diff --git a/website/src/components/footer.js b/website/src/components/footer.js new file mode 100644 index 00000000..0078b7b0 --- /dev/null +++ b/website/src/components/footer.js @@ -0,0 +1,97 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +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}; + } +`; + +function Footer({ buttons }) { + return ( + + + + + {buttons.map(btn => ( + + {btn.name} + + ))} + + +

    + + Distributed under MIT License + {' '} + ·{' '} + + Code of Conduct + +

    +
    +
    +
    +
    + ); +} + +export default Footer; diff --git a/website/src/components/github-button.js b/website/src/components/github-button.js new file mode 100644 index 00000000..10b92c2c --- /dev/null +++ b/website/src/components/github-button.js @@ -0,0 +1,32 @@ +import React, { PureComponent } from 'react'; + +class GitHubStarButton extends PureComponent { + async componentDidMount() { + const gitHubButtonModule = await import('github-buttons/dist/react'); + + this.GitHubButton = gitHubButtonModule.default; + + this.forceUpdate(); + } + + render() { + const GitHubButton = this.GitHubButton; + + if (!GitHubButton) { + return null; + } + + return ( + + Star + + ); + } +} + +export default GitHubStarButton; diff --git a/website/src/components/grid.js b/website/src/components/grid.js new file mode 100644 index 00000000..6ae93e0e --- /dev/null +++ b/website/src/components/grid.js @@ -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; diff --git a/website/src/components/header.js b/website/src/components/header.js new file mode 100644 index 00000000..c1170ae6 --- /dev/null +++ b/website/src/components/header.js @@ -0,0 +1,248 @@ +import React, { useState, useEffect } from 'react'; +import { Link, graphql, StaticQuery } 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 logo from '../img/simple-cms-logo.svg'; +import searchIcon from '../img/search.svg'; +import theme from '../theme'; +import { mq } from '../utils'; + +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; + + ${mq[2]} { + position: sticky; + top: 0; + width: 100%; + z-index: ${theme.zIndexes.header}; + + ${p => + !p.collapsed && + css` + background: #2a2c34; + padding-top: ${theme.space[5]}; + padding-bottom: ${theme.space[5]}; + `}; + + ${p => + p.hasNotifications && + css` + padding-top: 0; + `}; + } +`; + +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 StyledLink = styled(props => )` + display: flex; +`; + +const NOTIFS_QUERY = graphql` + query notifs { + file(relativePath: { regex: "/notifications/" }) { + childDataYaml { + notifications { + published + loud + message + url + } + } + } + } +`; + +function 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', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + function handleScroll() { + const currentWindowPos = document.documentElement.scrollTop || document.body.scrollTop; + + const scrolled = currentWindowPos > 0; + + setScrolled(scrolled); + } + + function handleMenuBtnClick() { + setNavOpen(s => !s); + setSearchOpen(false); + } + + function handleSearchBtnClick() { + setSearchOpen(s => !s); + setNavOpen(false); + } + + return ( + + {data => { + const notifications = data.file.childDataYaml.notifications.filter( + notif => notif.published, + ); + const collapsed = !hasHeroBelow || scrolled; + const hasNotifications = notifications.length > 0; + return ( + + + + + + Simple CMS logo + + + + + {isSearchOpen ? × : search} + + + {isNavOpen ? × : } + + + + + + + + + + Star + + + + Docs + + + Contributing + + + Community + + + Blog + + + + + + ); + }} + + ); +} + +export default Header; diff --git a/website/src/components/hero-title.js b/website/src/components/hero-title.js new file mode 100644 index 00000000..f7bf9e32 --- /dev/null +++ b/website/src/components/hero-title.js @@ -0,0 +1,17 @@ +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; diff --git a/website/src/components/home-section.js b/website/src/components/home-section.js new file mode 100644 index 00000000..1080c8b7 --- /dev/null +++ b/website/src/components/home-section.js @@ -0,0 +1,37 @@ +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; +`; + +function HomeSection({ title, text, children, ...props }) { + return ( + + +
    + {title} + {text && {text}} +
    + {children} +
    +
    + ); +} + +export default HomeSection; diff --git a/website/src/components/layout.js b/website/src/components/layout.js new file mode 100644 index 00000000..3bbba7f8 --- /dev/null +++ b/website/src/components/layout.js @@ -0,0 +1,71 @@ +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 GlobalStyles from '../global-styles'; +import theme from '../theme'; + +const LAYOUT_QUERY = graphql` + query layoutQuery { + site { + siteMetadata { + title + description + } + } + footer: file(relativePath: { regex: "/global/" }) { + childDataYaml { + footer { + buttons { + url + name + } + } + } + } + } +`; + +export function LayoutTemplate({ children }) { + return ( + + + {children} + + ); +} + +function Layout({ hasPageHero, children }) { + return ( + + {data => { + const { title, description } = data.site.siteMetadata; + + return ( + + + + + +
    + {children} +