├── bin ├── README.md └── backroad.js ├── doc └── README.md ├── admin ├── README.md ├── postcss.config.js ├── src │ ├── assets │ │ ├── scss │ │ │ ├── partials │ │ │ │ ├── _layout.scss │ │ │ │ ├── _footer.scss │ │ │ │ ├── _home.scss │ │ │ │ ├── _type.scss │ │ │ │ ├── _back.scss │ │ │ │ ├── _bg.scss │ │ │ │ ├── _view.scss │ │ │ │ ├── _login.scss │ │ │ │ ├── _fields.scss │ │ │ │ ├── _user-menu.scss │ │ │ │ ├── _sidebar.scss │ │ │ │ ├── _header.scss │ │ │ │ └── _tables.scss │ │ │ └── admin.scss │ │ └── img │ │ │ ├── bg-1.jpg │ │ │ ├── bg-2.jpg │ │ │ ├── bg-3.jpg │ │ │ ├── bg-4.jpg │ │ │ └── bg-5.jpg │ ├── components │ │ ├── layout │ │ │ ├── Footer.js │ │ │ ├── Sidebar.js │ │ │ ├── Header.js │ │ │ └── UserMenu.js │ │ ├── views │ │ │ ├── NotFound.js │ │ │ ├── ManageUsers.js │ │ │ ├── Home.js │ │ │ ├── NewUser.js │ │ │ ├── ManageArticles.js │ │ │ ├── NewArticle.js │ │ │ ├── EditUser.js │ │ │ └── EditArticle.js │ │ ├── elements │ │ │ ├── LogoutLink.js │ │ │ ├── RandomBackground.js │ │ │ ├── BackButton.js │ │ │ ├── Alert.js │ │ │ ├── Search.js │ │ │ ├── Button.js │ │ │ ├── NavItem.js │ │ │ ├── Notice.js │ │ │ ├── PrimaryNav.js │ │ │ └── Logo.js │ │ ├── tables │ │ │ ├── ArticlesTable.js │ │ │ ├── UsersTable.js │ │ │ ├── ArticleRow.js │ │ │ ├── UserRow.js │ │ │ ├── ManageTable.js │ │ │ └── RecentContentTable.js │ │ ├── fields │ │ │ ├── Checkbox.js │ │ │ ├── TextArea.js │ │ │ ├── SelectMenu.js │ │ │ ├── RadioGroup.js │ │ │ ├── Input.js │ │ │ ├── Fields.js │ │ │ ├── MarkdownEditor.js │ │ │ └── RichTextEditor.js │ │ └── forms │ │ │ ├── ArticleForm.js │ │ │ ├── LoginForm.js │ │ │ ├── UserForm.js │ │ │ └── EditForm.js │ ├── utils │ │ ├── user.js │ │ ├── authorized.js │ │ └── data.js │ ├── containers │ │ ├── Admin.js │ │ ├── PrivateRoute.js │ │ ├── Login.js │ │ ├── AdminRoute.js │ │ └── View.js │ ├── index.js │ ├── store │ │ ├── index.js │ │ ├── config.js │ │ ├── notice.js │ │ └── auth.js │ └── App.js ├── .eslintrc ├── public │ └── README.md └── webpack.config.js ├── lib ├── README.md ├── assets │ ├── scss │ │ ├── partials │ │ │ ├── _buttons.scss │ │ │ ├── _forms.scss │ │ │ └── _notifications.scss │ │ ├── backroad.scss │ │ └── utils │ │ │ └── _variables.scss │ └── README.md ├── utils │ ├── timing.js │ └── formatting.js ├── server │ ├── models │ │ ├── setting.js │ │ ├── article.js │ │ └── user.js │ ├── views │ │ ├── admin.hbs │ │ └── install.hbs │ ├── routes │ │ ├── config.js │ │ ├── admin.js │ │ ├── auth.js │ │ ├── content-types.js │ │ ├── install.js │ │ ├── articles.js │ │ └── users.js │ └── index.js ├── content-types.js └── backroad.js ├── install ├── README.md ├── postcss.config.js ├── .eslintrc ├── src │ └── assets │ │ ├── scss │ │ ├── install.scss │ │ └── partials │ │ │ └── _layout.scss │ │ └── js │ │ └── app.js ├── public │ └── README.md └── webpack.config.js ├── example ├── README.md └── basic │ ├── .env │ ├── package.json │ ├── package-lock.json │ ├── index.js │ └── content-types.js ├── index.js ├── screenshot-1.jpg ├── screenshot-2.jpg ├── screenshot-3.jpg ├── screenshot-4.jpg ├── screenshot-5.jpg ├── screenshot-6.jpg ├── screenshot-7.jpg ├── screenshot-8.jpg ├── .editorconfig ├── dist └── README.md ├── .gitignore ├── package.json └── README.md /bin/README.md: -------------------------------------------------------------------------------- 1 | # CLI Tools 2 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | -------------------------------------------------------------------------------- /admin/README.md: -------------------------------------------------------------------------------- 1 | # Admin Interface 2 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # Core Functionality 2 | -------------------------------------------------------------------------------- /bin/backroad.js: -------------------------------------------------------------------------------- 1 | // `backroad` command ... 2 | -------------------------------------------------------------------------------- /install/README.md: -------------------------------------------------------------------------------- 1 | # Installation Interface 2 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Working Examples and Use Cases 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/backroad'); 4 | -------------------------------------------------------------------------------- /lib/assets/scss/partials/_buttons.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons 3 | // 4 | 5 | // ... 6 | -------------------------------------------------------------------------------- /screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/screenshot-1.jpg -------------------------------------------------------------------------------- /screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/screenshot-2.jpg -------------------------------------------------------------------------------- /screenshot-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/screenshot-3.jpg -------------------------------------------------------------------------------- /screenshot-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/screenshot-4.jpg -------------------------------------------------------------------------------- /screenshot-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/screenshot-5.jpg -------------------------------------------------------------------------------- /screenshot-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/screenshot-6.jpg -------------------------------------------------------------------------------- /screenshot-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/screenshot-7.jpg -------------------------------------------------------------------------------- /screenshot-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/screenshot-8.jpg -------------------------------------------------------------------------------- /admin/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')] 3 | }; 4 | -------------------------------------------------------------------------------- /example/basic/.env: -------------------------------------------------------------------------------- 1 | SECRET=Sz14gJaNZV=CPO6?2M+-lbz2HG|R 7w#P,v8N J9IS5[GtUOK:Qro+RRR+-epi2| 2 | -------------------------------------------------------------------------------- /install/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')] 3 | }; 4 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_layout.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: $admin-body-bg-color; 3 | } 4 | -------------------------------------------------------------------------------- /admin/src/assets/img/bg-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/admin/src/assets/img/bg-1.jpg -------------------------------------------------------------------------------- /admin/src/assets/img/bg-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/admin/src/assets/img/bg-2.jpg -------------------------------------------------------------------------------- /admin/src/assets/img/bg-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/admin/src/assets/img/bg-3.jpg -------------------------------------------------------------------------------- /admin/src/assets/img/bg-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/admin/src/assets/img/bg-4.jpg -------------------------------------------------------------------------------- /admin/src/assets/img/bg-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeblvd/backroad/HEAD/admin/src/assets/img/bg-5.jpg -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_footer.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Footer 3 | // 4 | 5 | .app-footer { 6 | // ... 7 | } 8 | -------------------------------------------------------------------------------- /lib/assets/README.md: -------------------------------------------------------------------------------- 1 | # Shared Assets 2 | 3 | This directory contains assets shared between client interfaces. 4 | -------------------------------------------------------------------------------- /admin/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "plugin:jsx-a11y/recommended"], 3 | "plugins": ["jsx-a11y"] 4 | } 5 | -------------------------------------------------------------------------------- /install/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "plugin:jsx-a11y/recommended"], 3 | "plugins": ["jsx-a11y"] 4 | } 5 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_home.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Home Header 3 | // 4 | 5 | .home-top { 6 | margin: 0 0 30px 10px; 7 | 8 | h1 { 9 | font-size: 28px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_type.scss: -------------------------------------------------------------------------------- 1 | // 2 | // View Headings 3 | // 4 | 5 | .view-title { 6 | font-weight: 100; 7 | letter-spacing: 1px; 8 | margin-bottom: 40px; 9 | } 10 | -------------------------------------------------------------------------------- /install/src/assets/scss/install.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Shared 3 | // 4 | 5 | @import '../../../../lib/assets/scss/backroad.scss'; 6 | 7 | // 8 | // Install Page 9 | // 10 | 11 | @import 'partials/layout'; 12 | -------------------------------------------------------------------------------- /admin/public/README.md: -------------------------------------------------------------------------------- 1 | When running `@TODO` during development, the static admin client files are rendered to, and served from, this directory. 2 | 3 | You can view the development server at http://localhost/5050. 4 | -------------------------------------------------------------------------------- /install/public/README.md: -------------------------------------------------------------------------------- 1 | When running `@TODO` during development, the static install client files are rendered to, and served from, this directory. 2 | 3 | You can view the development server at http://localhost/5050. 4 | -------------------------------------------------------------------------------- /admin/src/components/layout/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => { 4 | return ( 5 |
6 |

Footer here...

7 |
8 | ); 9 | }; 10 | 11 | export default Footer; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [**/*.js] 4 | indent_style = space 5 | indent_size = 2 6 | quote_type = single 7 | max_line_length = 100 8 | 9 | [**/*.scss] 10 | indent_size = 2 11 | max_line_length = 100 12 | 13 | [*.json] 14 | indent_size = 2 15 | max_line_length = 200 16 | -------------------------------------------------------------------------------- /admin/src/components/layout/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PrimaryNav from '../elements/PrimaryNav'; 3 | 4 | const Sidebar = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | export default Sidebar; 13 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | Running `@TODO` will compile the final production build here. This includes a `server.js` file, along with a `/admin` and `/install` directories containing the static client-side files that it'll serve. 2 | 3 | You can test the production build with `@TODO` and viewing at `http://localhost/5050`. 4 | -------------------------------------------------------------------------------- /example/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "GPL-2.0+", 12 | "dependencies": { 13 | "dotenv": "^6.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_back.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Back Button 3 | // 4 | 5 | .btn-back.btn-default { 6 | background: #fff; 7 | box-shadow: $shadow-dark; 8 | position: fixed; 9 | top: $admin-header-height + 20px; 10 | left: $admin-sidebar-width + 20px; 11 | 12 | &:hover { 13 | background: $primary-color; 14 | color: #fff; 15 | } 16 | .icon { 17 | margin-right: 10px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /admin/src/utils/user.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | 3 | /** 4 | * Get gravatar image URL. 5 | * 6 | * @param {String} email Email address. 7 | * @return {String} Gravatar image URL. 8 | */ 9 | export function gravatar(email) { 10 | email = email.replace(/\s/g, ''); // Remove whitespace. 11 | email = email.toLowerCase(); 12 | return `https://www.gravatar.com/avatar/${md5(email)}.jpg?default=mp`; 13 | } 14 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_bg.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Random Background 3 | // 4 | 5 | .bg-random { 6 | overflow: hidden; 7 | position: fixed; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | 13 | img { 14 | position: absolute; 15 | top: 50%; 16 | left: 50%; 17 | width: 100%; 18 | height: 100%; 19 | object-fit: cover; 20 | transform: translate(-50%, -50%); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/basic/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "dotenv": { 8 | "version": "6.0.0", 9 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.0.0.tgz", 10 | "integrity": "sha512-FlWbnhgjtwD+uNLUGHbMykMOYQaTivdHEmYwAKFjn6GKe/CqY0fNae93ZHTd20snh9ZLr8mTzIL9m0APQ1pjQg==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /admin/src/components/views/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NotFound = () => { 4 | return ( 5 |
6 |
7 |

Page Not Found

8 |

9 | {"Oops! The page you're trying to reach doesn't exist. How embarrassing."} 10 |

11 |
12 |
13 | ); 14 | }; 15 | 16 | export default NotFound; 17 | -------------------------------------------------------------------------------- /admin/src/containers/Admin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../components/layout/Header'; 3 | import UserMenu from '../components/layout/UserMenu'; 4 | import Sidebar from '../components/layout/Sidebar'; 5 | import View from './View'; 6 | 7 | const Admin = () => { 8 | return ( 9 |
10 |
11 | 12 | 13 | 14 |
15 | ); 16 | }; 17 | 18 | export default Admin; 19 | -------------------------------------------------------------------------------- /example/basic/index.js: -------------------------------------------------------------------------------- 1 | const env = require('dotenv').config(); 2 | const backroad = require('../..'); // require('backroad') 3 | const { post, page, book } = require('./content-types'); 4 | 5 | /** 6 | * Create new application. 7 | */ 8 | const app = backroad(); 9 | 10 | /** 11 | * Add custom content types. 12 | */ 13 | app.contentTypes.add(post); 14 | app.contentTypes.add(page); 15 | app.contentTypes.add(book); 16 | 17 | /** 18 | * Run the application! 19 | */ 20 | app.start(); 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # reports 7 | report-client.txt 8 | report-server.txt 9 | 10 | # client 11 | admin/public/* 12 | !admin/public/README.md 13 | install/public/* 14 | !install/public/README.md 15 | 16 | # build 17 | dist/* 18 | !dist/README.md 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | .tags 27 | .tags1 28 | 29 | npm-debug.log* 30 | -------------------------------------------------------------------------------- /lib/utils/timing.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Timeout promise. 4 | * 5 | * This allows us to change timeouts together 6 | * with a promise. 7 | * 8 | * @param {Number} delay Time delay in milliseconds. 9 | * @param {Mixed} dataToPass Data to pass along the chain. 10 | */ 11 | timeoutPromise: function(delay, dataToPass) { 12 | return new Promise(function(resolve, reject) { 13 | setTimeout(function() { 14 | resolve(dataToPass); 15 | }, delay); 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /admin/src/assets/scss/admin.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Shared 3 | // 4 | 5 | @import '../../../../lib/assets/scss/backroad.scss'; 6 | 7 | // 8 | // Admin 9 | // 10 | 11 | @import 'partials/type'; 12 | @import 'partials/layout'; 13 | @import 'partials/bg'; 14 | @import 'partials/back'; 15 | @import 'partials/header'; 16 | @import 'partials/user-menu'; 17 | @import 'partials/sidebar'; 18 | @import 'partials/view'; 19 | @import 'partials/footer'; 20 | @import 'partials/fields'; 21 | @import 'partials/tables'; 22 | @import 'partials/home'; 23 | @import 'partials/login'; 24 | -------------------------------------------------------------------------------- /admin/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | // Routing 5 | import { BrowserRouter } from 'react-router-dom'; 6 | 7 | // Store 8 | import { Provider } from 'react-redux'; 9 | import store from './store'; 10 | 11 | // Application 12 | import './assets/scss/admin.scss'; 13 | import App from './App'; 14 | 15 | ReactDOM.render( 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ); 23 | -------------------------------------------------------------------------------- /admin/src/components/views/ManageUsers.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ManageTable from '../tables/ManageTable'; 3 | import UsersTable from '../tables/UsersTable'; 4 | 5 | /** 6 | * Manage Users 7 | * 8 | * @return {Component} 9 | */ 10 | const ManageUsers = props => { 11 | return ( 12 |
13 |
14 | } /> 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default ManageUsers; 21 | -------------------------------------------------------------------------------- /lib/assets/scss/backroad.scss: -------------------------------------------------------------------------------- 1 | // Required by Front Street 2 | $direction: 'ltr'; 3 | $version: 'core'; 4 | 5 | // Front Street Functions, Variables and Mixins 6 | @import '~frontstreet/src/scss/utils/index'; 7 | 8 | // Back Road configuration 9 | @import 'utils/variables'; 10 | 11 | // Front Street Core 12 | @import '~frontstreet/src/scss/core/index'; 13 | 14 | // Front Street Blocks 15 | @import '~frontstreet/src/scss/blocks/alert'; 16 | 17 | // Back Road Core 18 | @import 'partials/buttons'; 19 | @import 'partials/forms'; 20 | @import 'partials/notifications'; 21 | -------------------------------------------------------------------------------- /admin/src/containers/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | /** 6 | * Private Route 7 | * 8 | * This is a wrapper for that 9 | * restricts access to logged-in users. 10 | * 11 | * @return {Component} 12 | */ 13 | const PrivateRoute = props => { 14 | const { isAuthenticated, path, component } = props; 15 | return isAuthenticated ? : ; 16 | }; 17 | 18 | export default connect(state => state.auth)(PrivateRoute); 19 | -------------------------------------------------------------------------------- /install/src/assets/scss/partials/_layout.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: $install-body-bg-color; 3 | } 4 | 5 | .install-page { 6 | > .wrap { 7 | padding: 0 0 50px 0; 8 | margin: 0 auto; 9 | max-width: 600px; 10 | } 11 | .install-logo { 12 | padding: 80px 0 60px 0; 13 | } 14 | .logo { 15 | display: block; 16 | fill: #fff; 17 | margin: 0 auto; 18 | width: 180px; 19 | } 20 | form { 21 | background: $install-inner-bg-color; 22 | border-radius: 4px; 23 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.75); 24 | padding: 60px 80px; 25 | margin: 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /admin/src/components/views/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import RecentContentTable from '../tables/RecentContentTable'; 4 | 5 | const Home = props => { 6 | const { first_name, username } = props; 7 | 8 | return ( 9 |
10 |
11 |
12 |

Welcome, {first_name ? first_name : username}.

13 |
14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default connect(state => ({ ...state.auth }))(Home); 21 | -------------------------------------------------------------------------------- /admin/src/components/elements/LogoutLink.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { logout } from '../../store/auth'; 4 | 5 | class LogoutLink extends Component { 6 | handleClick = event => { 7 | event.preventDefault(); 8 | this.props.closeUserMenu(); 9 | this.props.logout(); 10 | }; 11 | 12 | render() { 13 | const { children } = this.props; 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | } 21 | 22 | export default connect( 23 | null, 24 | { logout } 25 | )(LogoutLink); 26 | -------------------------------------------------------------------------------- /admin/src/containers/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Logo from '../components/elements/Logo'; 3 | import LoginForm from '../components/forms/LoginForm'; 4 | import RandomBackground from '../components/elements/RandomBackground'; 5 | 6 | const Login = () => { 7 | return ( 8 |
9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 | ); 20 | }; 21 | 22 | export default Login; 23 | -------------------------------------------------------------------------------- /admin/src/components/elements/RandomBackground.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import bg1 from '../../assets/img/bg-1.jpg'; 3 | import bg2 from '../../assets/img/bg-2.jpg'; 4 | import bg3 from '../../assets/img/bg-3.jpg'; 5 | import bg4 from '../../assets/img/bg-4.jpg'; 6 | import bg5 from '../../assets/img/bg-5.jpg'; 7 | 8 | const RandomBackground = () => { 9 | const randomIndex = Math.floor(Math.random() * (4 - 0) + 0); // 0 - 4 10 | const images = [bg1, bg2, bg3, bg4, bg5]; 11 | 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | 19 | export default RandomBackground; 20 | -------------------------------------------------------------------------------- /lib/server/models/setting.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | /** 5 | * Creates a Mongoose schema for a setting 6 | * in the database. 7 | */ 8 | function createSettingSchema(config) { 9 | const schema = new Schema( 10 | { 11 | name: { 12 | type: String, 13 | unique: true, 14 | required: true 15 | }, 16 | value: { 17 | type: Schema.Types.Mixed 18 | } 19 | }, 20 | { collection: 'settings' } 21 | ); 22 | 23 | return mongoose.model('Setting', schema); 24 | } 25 | 26 | /** 27 | * Export factory function. 28 | */ 29 | module.exports = createSettingSchema; 30 | -------------------------------------------------------------------------------- /admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import auth from './auth'; 5 | import notice from './notice'; 6 | import config from './config'; 7 | 8 | const reducer = combineReducers({ 9 | auth, 10 | notice, 11 | config 12 | }); 13 | 14 | var store; 15 | 16 | if (process.env.NODE_ENV === 'development') { 17 | store = createStore( 18 | reducer, 19 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), 20 | applyMiddleware(thunk) 21 | ); 22 | } else { 23 | store = createStore(reducer, applyMiddleware(thunk)); 24 | } 25 | 26 | export default store; 27 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_view.scss: -------------------------------------------------------------------------------- 1 | // 2 | // View 3 | // 4 | 5 | .app-view { 6 | overflow: scroll; 7 | position: fixed; 8 | top: $admin-header-height; 9 | right: 0; 10 | bottom: 0; 11 | left: $admin-sidebar-width; 12 | } 13 | 14 | // 15 | // Narrow Views 16 | // 17 | 18 | .new-user-view, 19 | .edit-user-view, 20 | .new-article-view, 21 | .edit-article-view, 22 | .not-found-view { 23 | > .wrap { 24 | max-width: 1000px; 25 | margin: 0 auto; 26 | padding: 50px; 27 | } 28 | } 29 | 30 | // 31 | // Wide Views 32 | // 33 | 34 | .home-view, 35 | .manage-users-view, 36 | .manage-articles-view { 37 | max-width: 1400px; 38 | margin: 0 auto; 39 | padding: 50px; 40 | } 41 | -------------------------------------------------------------------------------- /lib/server/views/admin.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /admin/src/utils/authorized.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | /** 4 | * Create authorized axios instance. 5 | * 6 | * This allows us to pass around a single 7 | * instance of axios, which has the logged-in 8 | * user's authentication token in the header. 9 | * 10 | * @return {Object} Axios instance. 11 | */ 12 | function authorized() { 13 | const authorized = axios.create(); 14 | 15 | authorized.interceptors.request.use(function(config) { 16 | const token = localStorage.getItem('token'); 17 | 18 | if (token) { 19 | config.headers.Authorization = `Bearer ${token}`; 20 | } 21 | 22 | return config; 23 | }); 24 | 25 | return authorized; 26 | } 27 | 28 | export default authorized(); 29 | -------------------------------------------------------------------------------- /admin/src/components/views/NewUser.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { singularTitle } from '../../utils/data'; 3 | import BackButton from '../elements/BackButton'; 4 | import EditForm from '../forms/EditForm'; 5 | import UserForm from '../forms/UserForm'; 6 | 7 | /** 8 | * Add a new user. 9 | * 10 | * @return {Component} 11 | */ 12 | const newUser = props => { 13 | return ( 14 |
15 |
16 |

New {singularTitle('users')}

17 | 18 | } /> 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default newUser; 25 | -------------------------------------------------------------------------------- /admin/src/containers/AdminRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | /** 6 | * Admin Route 7 | * 8 | * Restricts access to routes that careful 9 | * only accessible by and admin-level user. 10 | * 11 | * NOTE: A non-logged user will never get 12 | * to this component; so only a check for 13 | * the role of current user is needed. 14 | * 15 | * @return {Component} 16 | */ 17 | const AdminRoute = props => { 18 | const { isAdmin, path, component } = props; 19 | return isAdmin ? : ; 20 | }; 21 | 22 | export default connect(state => ({ isAdmin: state.auth.role === 'admin' }))(AdminRoute); 23 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_login.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Login Page 3 | // 4 | 5 | .login-page { 6 | background: $bg-color-inverse; 7 | min-height: 100vh; 8 | 9 | > .wrap { 10 | margin: 0 auto; 11 | max-width: 450px; 12 | padding: 40px 0 0 0; 13 | } 14 | .login-logo { 15 | padding: 40px; 16 | position: relative; 17 | text-align: center; 18 | z-index: 2; 19 | } 20 | .login-logo .logo { 21 | fill: #fff; 22 | width: 160px; 23 | } 24 | .login-form { 25 | background: $login-form-bg-color; 26 | border-radius: 4px; 27 | box-shadow: 0 0 50px rgba(0, 0, 0, 0.25); 28 | padding: 50px; 29 | position: relative; 30 | z-index: 2; 31 | } 32 | .bg-random { 33 | opacity: 0.5; 34 | z-index: 1; 35 | } 36 | .bg-random img { 37 | filter: blur(50px); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /admin/src/components/elements/BackButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 4 | import faLongArrowAltLeft from '@fortawesome/fontawesome-free-solid/faLongArrowAltLeft'; 5 | 6 | /** 7 | * Back Button 8 | * 9 | * Displayed when editing or adding users 10 | * and documents, to go back to the respective 11 | * management table. 12 | * 13 | * @param {Object} props Component props. 14 | * @param {Function} props.to Where to redirect. 15 | * @return {Component} 16 | */ 17 | const BackButton = props => { 18 | const { to } = props; 19 | 20 | return ( 21 | 22 | 23 | Back 24 | 25 | ); 26 | }; 27 | 28 | export default BackButton; 29 | -------------------------------------------------------------------------------- /admin/src/store/config.js: -------------------------------------------------------------------------------- 1 | import authorized from '../utils/authorized.js'; 2 | 3 | // Intial State 4 | 5 | const initialState = { 6 | docTypes: [], 7 | optionGroups: [] 8 | }; 9 | 10 | // Action Types 11 | 12 | const LOAD_CONFIG = 'LOAD_CONFIG'; 13 | 14 | // Reducer 15 | 16 | export default function reducer(state = initialState, action) { 17 | switch (action.type) { 18 | case LOAD_CONFIG: 19 | return { ...action.config }; 20 | 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | // Actions 27 | 28 | export function loadConfig() { 29 | return dispatch => { 30 | authorized 31 | .get('/api/v1/config') 32 | .then(response => { 33 | dispatch({ 34 | type: LOAD_CONFIG, 35 | config: response.data 36 | }); 37 | }) 38 | .catch(err => { 39 | console.log(err); 40 | }); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /admin/src/components/elements/Alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Alert 5 | * 6 | * This is for displaying messages within 7 | * components like forms, not to be confused 8 | * with the "Notice" component which is for 9 | * top-level application notices. 10 | * 11 | * @param {Object} props Component props. 12 | * @param {String} props.status Status, `sucess`, `info`, `warning`, or `danger` 13 | * @param {String} props.title Optional. Heading for alert. 14 | * @param {String} props.text Content of alert. 15 | * @return {Component} 16 | */ 17 | const Alert = props => { 18 | const { status, title, text } = props; 19 | 20 | return ( 21 |
22 |

23 | {title && {title + ' '}} 24 | {text} 25 |

26 |
27 | ); 28 | }; 29 | 30 | export default Alert; 31 | -------------------------------------------------------------------------------- /admin/src/components/views/ManageArticles.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { getPathBase, isValidContentType } from '../../utils/data'; 4 | import NotFound from './NotFound'; 5 | import ManageTable from '../tables/ManageTable'; 6 | import ArticlesTable from '../tables/ArticlesTable'; 7 | 8 | /** 9 | * Manage Articles 10 | * 11 | * @return {Component} 12 | */ 13 | const ManageArticles = props => { 14 | const { location } = props; 15 | const type = getPathBase(location.pathname); 16 | 17 | if (!isValidContentType(type)) { 18 | return ; 19 | } 20 | 21 | return ( 22 |
23 |
24 | } /> 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default withRouter(ManageArticles); 31 | -------------------------------------------------------------------------------- /admin/src/components/views/NewArticle.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { singularTitle, getPathBase } from '../../utils/data'; 4 | import BackButton from '../elements/BackButton'; 5 | import EditForm from '../forms/EditForm'; 6 | import ArticleForm from '../forms/ArticleForm'; 7 | 8 | /** 9 | * Add a new document (of any content 10 | * type). 11 | * 12 | * @return {Component} 13 | */ 14 | const NewDoc = props => { 15 | const { type } = props.match.params; 16 | const title = 'New ' + singularTitle(type); 17 | return ( 18 |
19 |
20 |

{title}

21 | 22 | } /> 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default withRouter(NewDoc); 29 | -------------------------------------------------------------------------------- /admin/src/store/notice.js: -------------------------------------------------------------------------------- 1 | // Intial State 2 | const initialState = { 3 | hasNotice: false, 4 | message: '', 5 | type: '' 6 | }; 7 | 8 | // Action Types 9 | 10 | const ADD_NOTICE = 'ADD_NOTICE'; 11 | 12 | const REMOVE_NOTICE = 'REMOVE_NOTICE'; 13 | 14 | // Reducer 15 | 16 | export default function reducer(state = initialState, action) { 17 | switch (action.type) { 18 | case ADD_NOTICE: 19 | return { 20 | hasNotice: true, 21 | message: action.noticeMessage, 22 | type: action.noticeType 23 | }; 24 | 25 | case REMOVE_NOTICE: 26 | return initialState; 27 | 28 | default: 29 | return state; 30 | } 31 | } 32 | 33 | // Actions 34 | 35 | export function addNotice(noticeMessage, noticeType) { 36 | return { 37 | type: ADD_NOTICE, 38 | noticeMessage, 39 | noticeType 40 | }; 41 | } 42 | 43 | export function removeNotice() { 44 | return { 45 | type: REMOVE_NOTICE 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /admin/src/components/tables/ArticlesTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ArticleRow from './ArticleRow'; 3 | 4 | /** 5 | * Users Table 6 | * 7 | * @return {Component} 8 | */ 9 | const ArticlesTable = props => { 10 | const { items, type } = props; 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {items.map(item => ( 24 | 33 | ))} 34 | 35 |
TitleCreatedCreated ByActions
36 | ); 37 | }; 38 | 39 | export default ArticlesTable; 40 | -------------------------------------------------------------------------------- /admin/src/components/tables/UsersTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UserRow from './UserRow'; 3 | 4 | /** 5 | * Users Table 6 | * 7 | * @return {Component} 8 | */ 9 | const UsersTable = props => { 10 | const { items } = props; 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {items.map(item => ( 25 | 34 | ))} 35 | 36 |
UsernameNameEmailRoleActions
37 | ); 38 | }; 39 | 40 | export default UsersTable; 41 | -------------------------------------------------------------------------------- /admin/src/components/views/EditUser.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import { singularTitle } from '../../utils/data'; 5 | import EditForm from '../forms/EditForm'; 6 | import BackButton from '../elements/BackButton'; 7 | import UserForm from '../forms/UserForm'; 8 | 9 | /** 10 | * Edit a user. 11 | * 12 | * @return {Component} 13 | */ 14 | const EditUser = props => { 15 | const username = props.match.params.username ? props.match.params.username : props.currentUser; 16 | 17 | return ( 18 |
19 |
20 |

Edit {singularTitle('users')}

21 | 22 | } /> 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default withRouter(connect(state => ({ currentUser: state.auth.username }))(EditUser)); 29 | -------------------------------------------------------------------------------- /admin/src/components/views/EditArticle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { isValidContentType, singularTitle } from '../../utils/data'; 4 | import BackButton from '../elements/BackButton'; 5 | import NotFound from './NotFound'; 6 | import EditForm from '../forms/EditForm'; 7 | import ArticleForm from '../forms/ArticleForm'; 8 | 9 | /** 10 | * Edit a document (of any content type). 11 | * 12 | * @return {Component} 13 | */ 14 | const EditArticle = props => { 15 | const { type, slug } = props.match.params; 16 | 17 | if (!isValidContentType(type)) { 18 | return ; 19 | } 20 | 21 | return ( 22 |
23 |
24 |

Edit {singularTitle(type)}

25 | 26 | } /> 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default withRouter(EditArticle); 33 | -------------------------------------------------------------------------------- /lib/assets/scss/partials/_forms.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Fields 3 | // 4 | 5 | #{$fs-field-selector} { 6 | background-color: #fff; 7 | border: none; 8 | border-radius: 4px; 9 | box-shadow: $field-shadow; 10 | color: $text-color-darken; 11 | transition: box-shadow 0.1s ease-in, transform 0.1s ease-in; 12 | 13 | &:disabled { 14 | background: #f5f5f5; 15 | color: lighten($text-color-lighten, 10%); 16 | font-style: italic; 17 | } 18 | &:focus { 19 | box-shadow: $field-shadow-focus; 20 | } 21 | } 22 | 23 | textarea { 24 | line-height: 1.5; 25 | } 26 | 27 | // 28 | // Labels/Legends 29 | // 30 | 31 | label, 32 | legend { 33 | font-size: 16px; 34 | margin: 0 4px 15px 4px; 35 | } 36 | .help-text { 37 | display: block; 38 | font-size: 14px; 39 | margin-top: -5px; 40 | } 41 | 42 | // 43 | // Field Error 44 | // 45 | 46 | .field-error { 47 | input { 48 | border-color: darken(adjust-hue($danger-color, -10), 20%); 49 | } 50 | } 51 | input.error { 52 | border-color: darken(adjust-hue($danger-color, -10), 20%); 53 | } 54 | -------------------------------------------------------------------------------- /admin/src/components/elements/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 3 | import faSearch from '@fortawesome/fontawesome-free-solid/faSearch'; 4 | import classNames from 'classnames'; 5 | 6 | class Search extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { active: false }; 10 | } 11 | handleFocus = () => { 12 | this.setState({ active: true }); 13 | }; 14 | handleBlur = () => { 15 | this.setState({ active: false }); 16 | }; 17 | render() { 18 | const className = classNames({ 19 | 'app-search': true, 20 | 'field-icon': true, 21 | 'field-icon-sm': true, 22 | active: this.state.active 23 | }); 24 | return ( 25 |
26 | 27 | 28 |
29 | ); 30 | } 31 | } 32 | 33 | export default Search; 34 | -------------------------------------------------------------------------------- /admin/src/components/fields/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Single Checkbox 5 | * 6 | * @param {Object} props Component props. 7 | * @param {String} props.value Current value. 8 | * @param {Function} props.onChange Handle input state change. 9 | * @param {String} props.title Optional. Title above input. 10 | * @param {String} props.name Name attribute for input. 11 | * @param {String} props.help, Optional. Help text below input. 12 | * @param {Boolean} props.hasError Whether currently has error. 13 | * @return {Component} 14 | */ 15 | const Checkbox = props => { 16 | const { value, onChange, title, name, help, hasError } = props; 17 | 18 | return ( 19 |

20 | {title && } 21 | 22 | {help && } 23 |

24 | ); 25 | }; 26 | 27 | export default Checkbox; 28 | -------------------------------------------------------------------------------- /lib/server/models/article.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const slugHero = require('mongoose-slug-hero'); 4 | 5 | /** 6 | * Creates a Mongoose schema for an article 7 | * in the database. 8 | */ 9 | function createArticleSchema(config) { 10 | const options = { 11 | minimize: false, 12 | collection: 'articles', 13 | timestamps: { 14 | createdAt: 'created_at', 15 | updatedAt: 'updated_at' 16 | } 17 | }; 18 | 19 | const model = { 20 | content_type: { 21 | type: 'String', 22 | required: true 23 | }, 24 | title: { 25 | type: String, 26 | required: true 27 | }, 28 | created_by: { 29 | type: String, 30 | required: true 31 | }, 32 | fields: { 33 | type: Object, 34 | default: {} 35 | } 36 | }; 37 | 38 | const schema = new Schema(model, options); 39 | 40 | schema.plugin(slugHero, { doc: 'Article', field: 'title' }); 41 | 42 | return mongoose.model('Article', schema); 43 | } 44 | 45 | /** 46 | * Export factory function. 47 | */ 48 | module.exports = createArticleSchema; 49 | -------------------------------------------------------------------------------- /admin/src/components/elements/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | /** 5 | * Button 6 | * 7 | * @param {Object} props Component props. 8 | * @param {String} props.size Optioonal button size, like `xs`, `sm`, `lg`, `xl`, `xxl`, or `xxxl`. 9 | * @param {Boolean} props.isLoading Whether button has loading animation state. 10 | * @param {Boolean} props.isPrimary Whether button has primary styling. 11 | * @param {Function} props.onClick Button click handler. 12 | * @return {Component} 13 | */ 14 | const Button = props => { 15 | const { size, isLoading, isPrimary, onClick, children } = props; 16 | 17 | const classes = classNames({ 18 | btn: true, 19 | 'btn-xs': size === 'xs', 20 | 'btn-sm': size === 'sm', 21 | 'btn-lg': size === 'lg', 22 | 'btn-xl': size === 'xl', 23 | 'btn-xxl': size === 'xxl', 24 | 'btn-xxxl': size === 'xxxl', 25 | 'has-loader': true, 26 | 'btn-primary': isPrimary, 27 | 'has-loader': true, 28 | loading: isLoading 29 | }); 30 | 31 | return ( 32 | 35 | ); 36 | }; 37 | 38 | export default Button; 39 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_fields.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Title Field 3 | // 4 | 5 | .new-article-view, 6 | .edit-article-view { 7 | .title-field { 8 | margin-bottom: 30px; 9 | } 10 | } 11 | 12 | // 13 | // Editors 14 | // 15 | 16 | .markdown-editor, 17 | .rich-text-editor { 18 | > .wrap { 19 | background-color: $field-bg-color; 20 | border-radius: $field-radius; 21 | box-shadow: $field-shadow; 22 | padding: 30px; 23 | margin-bottom: $fs-element-spacing; 24 | } 25 | .editor { 26 | min-height: 200px; 27 | } 28 | } 29 | 30 | .rich-text-editor { 31 | .toolbar-menu { 32 | border-bottom: 2px solid #eee; 33 | margin: 0 -20px; 34 | margin-bottom: 20px; 35 | padding: 1px 18px 17px; 36 | position: relative; 37 | 38 | .button { 39 | color: lighten($text-color-lighten, 15%); 40 | cursor: pointer; 41 | display: inline-block; 42 | font-size: 18px; 43 | } 44 | .button:hover { 45 | color: $text-color-lighten; 46 | } 47 | .button[data-active='true'] { 48 | color: $text-color-darken; 49 | } 50 | .button span { 51 | margin-left: 15px; 52 | } 53 | .button:first-child span { 54 | margin-left: 0; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/basic/content-types.js: -------------------------------------------------------------------------------- 1 | const post = { 2 | id: 'post', 3 | endpoint: 'posts', 4 | name: 'Post', 5 | pluralName: 'Posts', 6 | fields: [ 7 | { 8 | id: 'content', 9 | name: 'Content', 10 | type: 'rich-text', 11 | placeholder: 'Start writing...' 12 | }, 13 | { 14 | id: 'category', 15 | type: 'select', 16 | options: ['Tutorials', 'Rants', 'Adventures'] 17 | } 18 | ] 19 | }; 20 | 21 | const page = { 22 | id: 'page', 23 | endpoint: 'pages', 24 | name: 'Page', 25 | pluralName: 'Pages', 26 | fields: [ 27 | { 28 | id: 'content', 29 | type: 'rich-text', 30 | placeholder: 'Start writing...' 31 | } 32 | ] 33 | }; 34 | 35 | const book = { 36 | id: 'book', 37 | endpoint: 'books', 38 | name: 'Book', 39 | pluralName: 'Books', 40 | fields: [ 41 | { 42 | id: 'author', 43 | name: 'Author', 44 | type: 'text' 45 | }, 46 | { 47 | id: 'desc', 48 | name: 'Description', 49 | type: 'textarea' 50 | }, 51 | { 52 | id: 'genre', 53 | name: 'Genre', 54 | type: 'select', 55 | default: 'General', 56 | options: ['General', 'Adventure', 'Suspense', 'Romance', 'Comedy'] 57 | } 58 | ] 59 | }; 60 | 61 | module.exports = { post, page, book }; 62 | -------------------------------------------------------------------------------- /admin/src/components/layout/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { gravatar } from '../../utils/user'; 4 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 5 | import faChevronDown from '@fortawesome/fontawesome-free-solid/faChevronDown'; 6 | import Logo from '../elements/Logo'; 7 | import Search from '../elements/Search'; 8 | 9 | class Header extends Component { 10 | /** 11 | * Toggle the user menu. 12 | */ 13 | toggleUserMenu = event => { 14 | event.preventDefault(); 15 | document.body.classList.toggle('user-menu-on'); 16 | }; 17 | 18 | /** 19 | * Render component. 20 | * 21 | * @return {Component} 22 | */ 23 | render() { 24 | const { username, email } = this.props; 25 | 26 | return ( 27 |
28 |
29 | 30 | {'0.1.0'} 31 |
32 | 33 | 34 | 35 | {`Avatar 36 | 37 |
38 | ); 39 | } 40 | } 41 | 42 | export default connect(state => ({ ...state.auth }))(Header); 43 | -------------------------------------------------------------------------------- /admin/src/assets/scss/partials/_user-menu.scss: -------------------------------------------------------------------------------- 1 | // 2 | // User Menu 3 | // 4 | 5 | .app-user-menu { 6 | box-shadow: none; 7 | background: #fff; 8 | border-radius: 4px; 9 | opacity: 0; 10 | padding: 10px 0; 11 | position: fixed; 12 | top: -9999px; 13 | right: 10px; 14 | transform: scale(0); 15 | transform-origin: 80% 0%; 16 | transition: box-shadow 0.4s ease-in-out, transform 0.4s ease-in-out, opacity 0.4s ease-in-out; 17 | width: 150px; 18 | z-index: $user-menu-z-index; 19 | 20 | /* 21 | li:last-child { 22 | border-top: 1px solid $border-color; 23 | margin-top: 5px; 24 | padding-top: 5px; 25 | } 26 | */ 27 | ul { 28 | list-style: none; 29 | margin: 0; 30 | padding: 0; 31 | } 32 | a { 33 | color: $text-color; 34 | display: block; 35 | font-size: 14px; 36 | line-height: 30px; 37 | padding: 0 20px; 38 | text-decoration: none; 39 | } 40 | a:hover, 41 | a:focus { 42 | background-color: $bg-color-darken; 43 | color: $text-color-darken; 44 | } 45 | .svg-inline--fa { 46 | display: inline-block; 47 | margin-right: 10px; 48 | text-align: center; 49 | width: 1.2em; 50 | } 51 | } 52 | .user-menu-on .app-user-menu { 53 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); 54 | opacity: 1; 55 | top: $admin-header-height - 5px; 56 | transform: scale(1); 57 | } 58 | -------------------------------------------------------------------------------- /lib/utils/formatting.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Format a unified configuration object. 4 | * 5 | * @param {Object} config Mash of config pieces to wrangle. 6 | * @param {Object} config.app Application instance configuration. 7 | * @param {Object} config.contentTypes Content types API instance. 8 | * @param {Object} config.options Options API instance. 9 | */ 10 | toPublicConfig: function(config) { 11 | const { app, contentTypes, options } = config; 12 | 13 | const publicConfig = { 14 | adminTitle: app.adminTitle, 15 | contentTypes: contentTypes.get() 16 | // options: options.get() // @TODO 17 | }; 18 | 19 | return publicConfig; 20 | }, 21 | /** 22 | * Format a string into a slug. 23 | * 24 | * For example: 25 | * `Foo` => `foo` 26 | * `Foo Bar` => `foo-bar` 27 | * 28 | * @param {String} str String to format. 29 | * @return {String} Formatted string. 30 | */ 31 | toSlug: function(str) { 32 | return str 33 | .toString() 34 | .toLowerCase() 35 | .replace(/\s+/g, '-') // Replace spaces with - 36 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 37 | .replace(/\-\-+/g, '-') // Replace multiple - with single - 38 | .replace(/^-+/, '') // Trim - from start of text 39 | .replace(/-+$/, ''); // Trim - from end of text 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /admin/src/components/elements/NavItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { singularTitle, pluralTitle, getPathBase, getPathItem } from '../../utils/data'; 4 | 5 | /** 6 | * Navigation Item 7 | * 8 | * @param {Object} props Component properties. 9 | * @param {String} props.text Optional. Text of parent menu item. 10 | * @param {String} props.endpoint Base URL endpoint of item, like `pages`. 11 | * @param {String} props.currentPath Current pathname from React router. 12 | * @return {Component} 13 | */ 14 | const NavItem = props => { 15 | const { endpoint, text, currentPath, hasChildren } = props; 16 | const base = getPathBase(currentPath); 17 | const item = getPathItem(currentPath); 18 | 19 | return ( 20 |
  • 21 | {text ? text : pluralTitle(endpoint)} 22 | {hasChildren && ( 23 |
      24 |
    • 25 | Manage {pluralTitle(endpoint)} 26 |
    • 27 |
    • 28 | New {singularTitle(endpoint)} 29 |
    • 30 |
    31 | )} 32 |
  • 33 | ); 34 | }; 35 | 36 | export default NavItem; 37 | -------------------------------------------------------------------------------- /admin/src/components/fields/TextArea.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Textarea 5 | * 6 | * @param {Object} props Component props. 7 | * @param {String} props.value Current value. 8 | * @param {Function} props.onChange Handle input state change. 9 | * @param {String} props.title Optional. Title above input. 10 | * @param {String} props.name Name attribute for input. 11 | * @param {String} props.placeholder Optional. Field placeholder text. 12 | * @param {String} props.help, Optional. Help text below input. 13 | * @param {Boolean} props.hasError Whether currently has error. 14 | * @param {Boolean} props.isRequired Whether field is required. 15 | * @return {Component} 16 | */ 17 | const TextArea = props => { 18 | const { value, onChange, title, name, placeholder, help, hasError, isRequired } = props; 19 | 20 | return ( 21 |

    22 | {title && ( 23 | 27 | )} 28 |