├── .env.dev ├── .eslintignore ├── .eslintrc ├── .github ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── actions │ ├── assetActions.js │ ├── entryActions.js │ ├── fetchData.js │ ├── fieldActions.js │ ├── pageActions.js │ ├── pluginActions.js │ ├── sectionActions.js │ ├── siteActions.js │ ├── uiActions.js │ ├── userActions.js │ └── usergroupActions.js ├── assets │ ├── android-chrome-192x192.png │ ├── android-chrome-256x256.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── default_user.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.png │ ├── mstile-150x150.png │ └── safari-pinned-tab.svg ├── components │ ├── Avatar │ │ └── index.js │ ├── Breadcrumbs │ │ ├── Breadcrumbs.scss │ │ └── index.js │ ├── Button │ │ ├── Button.scss │ │ └── index.js │ ├── Checkbox │ │ ├── Checkbox.scss │ │ ├── Checkboxes.js │ │ └── index.js │ ├── DatePicker │ │ ├── DatePicker.scss │ │ ├── DayTile.js │ │ ├── Days.js │ │ └── index.js │ ├── DeleteIcon │ │ └── index.js │ ├── DropdownButton │ │ ├── DropdownButtons.scss │ │ └── index.js │ ├── FieldOptions │ │ └── index.js │ ├── Fields │ │ ├── Asset │ │ │ ├── Asset.scss │ │ │ ├── AssetModal.js │ │ │ └── index.js │ │ ├── Color │ │ │ ├── Color.scss │ │ │ └── index.js │ │ ├── Dropdown │ │ │ ├── Dropdown.scss │ │ │ └── index.js │ │ ├── Group │ │ │ ├── FieldColumn.js │ │ │ ├── Group.scss │ │ │ ├── GroupTile.js │ │ │ ├── NewBlockModal.js │ │ │ ├── Panel.js │ │ │ ├── Panel.scss │ │ │ └── index.js │ │ ├── Numeric │ │ │ └── index.js │ │ ├── RichText │ │ │ ├── RichText.scss │ │ │ └── index.js │ │ ├── Toggle │ │ │ ├── Toggle.scss │ │ │ └── index.js │ │ └── index.js │ ├── FileInput │ │ ├── FileInput.scss │ │ └── index.js │ ├── FlintLogo │ │ ├── FlintLogo.scss │ │ └── index.js │ ├── Input │ │ ├── Input.scss │ │ └── index.js │ ├── List │ │ ├── List.scss │ │ └── index.js │ ├── Loading │ │ ├── Loading.scss │ │ └── index.js │ ├── Modals │ │ └── ConfirmModal.js │ ├── Notification │ │ ├── Notification.scss │ │ └── index.js │ ├── SecondaryNav │ │ ├── SecondaryNav.scss │ │ └── index.js │ ├── StatusDot │ │ ├── StatusDot.scss │ │ └── index.js │ ├── Table │ │ ├── Cell.js │ │ ├── THead.js │ │ ├── Table.scss │ │ └── index.js │ ├── TitleBar │ │ ├── TitleBar.scss │ │ └── index.js │ └── Toast │ │ ├── Toast.scss │ │ └── index.js ├── containers │ ├── Aside │ │ ├── Aside.scss │ │ └── index.js │ ├── Empty │ │ ├── Empty.scss │ │ └── index.js │ ├── ErrorContainer │ │ ├── ErrorContainer.scss │ │ └── index.js │ ├── FieldLayout │ │ ├── FieldLayout.scss │ │ ├── FieldSource.js │ │ ├── FieldTarget.js │ │ ├── FieldTargetCard.js │ │ ├── index.js │ │ └── utils │ │ │ ├── collect.js │ │ │ ├── constants.js │ │ │ ├── source.js │ │ │ └── target.js │ ├── Footer │ │ ├── Footer.scss │ │ └── index.js │ ├── LoginContainer │ │ ├── LoginContainer.scss │ │ └── index.js │ ├── Main │ │ ├── Main.scss │ │ └── index.js │ ├── MainNav │ │ ├── MainNav.scss │ │ └── index.js │ ├── Modals │ │ ├── Modals.scss │ │ └── index.js │ └── Page │ │ ├── Page.scss │ │ └── index.js ├── index.tpl.html ├── main.js ├── main.scss ├── manifest.json ├── reducers │ ├── assets.js │ ├── entries.js │ ├── fields.js │ ├── pages.js │ ├── plugins.js │ ├── sections.js │ ├── site.js │ ├── ui.js │ ├── user.js │ ├── usergroups.js │ └── users.js ├── scss │ ├── normalize.scss │ └── tools │ │ ├── mixins.scss │ │ ├── tools.scss │ │ └── variables.scss ├── utils │ ├── camelcase.js │ ├── formatFields.js │ ├── getUserPermissions.js │ ├── graphFetcher.js │ ├── helpers.js │ ├── icons.js │ ├── permissionsQuery.js │ ├── prettyNames.js │ ├── renderOption.js │ ├── rootReducer.js │ ├── socketEvents.js │ ├── store.js │ ├── types.js │ └── validateFields.js └── views │ ├── 404 │ └── index.js │ ├── Assets │ ├── Asset │ │ └── index.js │ ├── Assets │ │ └── index.js │ └── NewAsset │ │ └── index.js │ ├── Auth │ ├── ForgotPassword │ │ └── index.js │ ├── Install │ │ ├── Install.scss │ │ └── index.js │ ├── Login │ │ └── index.js │ └── SetPassword │ │ └── index.js │ ├── Entries │ ├── Entries │ │ └── index.js │ ├── Entry │ │ └── index.js │ └── NewEntry │ │ └── index.js │ ├── Fields │ ├── Field │ │ └── index.js │ ├── Fields │ │ └── index.js │ └── NewField │ │ └── index.js │ ├── Home │ ├── Home.scss │ └── index.js │ ├── Pages │ ├── NewPage │ │ └── index.js │ ├── Page │ │ └── index.js │ ├── Pages │ │ └── index.js │ └── SettingsPage │ │ └── index.js │ ├── Sections │ ├── NewSection │ │ └── index.js │ ├── Section │ │ └── index.js │ └── Sections │ │ └── index.js │ ├── Settings │ ├── Logs │ │ ├── Logs.scss │ │ └── index.js │ ├── Plugins │ │ └── index.js │ ├── Settings.scss │ ├── Site │ │ └── index.js │ ├── Styles │ │ ├── CodeMirror.scss │ │ ├── Styles.scss │ │ └── index.js │ └── index.js │ ├── UserGroups │ ├── NewUserGroup │ │ └── index.js │ ├── UserGroup │ │ └── index.js │ └── UserGroups │ │ └── index.js │ └── Users │ ├── NewUser │ └── index.js │ ├── User │ └── index.js │ └── Users │ └── index.js ├── codecov.yml ├── config ├── constants.js ├── webpack.config.js └── webpack.production.config.js ├── dev.js ├── index.js ├── package-lock.json ├── package.json ├── server ├── apps │ ├── admin.js │ ├── api.js │ ├── graphql.js │ └── routes │ │ ├── assets.js │ │ ├── auth.js │ │ ├── logs.js │ │ └── site.js ├── graphql │ ├── get-projection.js │ ├── index.js │ ├── mutations │ │ ├── assets │ │ │ ├── addAsset.js │ │ │ ├── index.js │ │ │ ├── indexAssets.js │ │ │ ├── removeAsset.js │ │ │ └── updateAsset.js │ │ ├── entries │ │ │ ├── addEntry.js │ │ │ ├── index.js │ │ │ ├── removeEntry.js │ │ │ └── updateEntry.js │ │ ├── fields │ │ │ ├── addField.js │ │ │ ├── index.js │ │ │ ├── removeField.js │ │ │ └── updateField.js │ │ ├── index.js │ │ ├── pages │ │ │ ├── addPage.js │ │ │ ├── index.js │ │ │ ├── removePage.js │ │ │ └── updatePage.js │ │ ├── sections │ │ │ ├── addSection.js │ │ │ ├── index.js │ │ │ ├── removeSection.js │ │ │ └── updateSection.js │ │ ├── site │ │ │ ├── index.js │ │ │ └── updateSite.js │ │ ├── usergroups │ │ │ ├── addUserGroup.js │ │ │ ├── index.js │ │ │ ├── removeUserGroup.js │ │ │ └── updateUserGroup.js │ │ └── users │ │ │ ├── addUser.js │ │ │ ├── deleteUser.js │ │ │ ├── index.js │ │ │ ├── resetPassword.js │ │ │ └── updateUser.js │ ├── queries │ │ ├── assets │ │ │ ├── index.js │ │ │ ├── multiple.js │ │ │ └── single.js │ │ ├── entries │ │ │ ├── index.js │ │ │ ├── multiple.js │ │ │ └── single.js │ │ ├── fields │ │ │ ├── index.js │ │ │ ├── multiple.js │ │ │ └── single.js │ │ ├── index.js │ │ ├── pages │ │ │ ├── index.js │ │ │ ├── multiple.js │ │ │ └── single.js │ │ ├── plugins │ │ │ ├── index.js │ │ │ └── multiple.js │ │ ├── sections │ │ │ ├── index.js │ │ │ ├── multiple.js │ │ │ └── single.js │ │ ├── site │ │ │ ├── index.js │ │ │ └── site.js │ │ ├── usergroups │ │ │ ├── index.js │ │ │ ├── multiple.js │ │ │ └── single.js │ │ └── users │ │ │ ├── index.js │ │ │ ├── multiple.js │ │ │ └── single.js │ └── types │ │ ├── Assets.js │ │ ├── CustomTypes │ │ ├── DateTime.js │ │ ├── FieldType.js │ │ ├── ObjectType.js │ │ └── index.js │ │ ├── Entries.js │ │ ├── Fields.js │ │ ├── Pages.js │ │ ├── Plugins.js │ │ ├── Sections.js │ │ ├── Site.js │ │ ├── UserGroups.js │ │ └── Users.js ├── index.js ├── models │ ├── AssetSchema.js │ ├── EntrySchema.js │ ├── FieldSchema.js │ ├── PageSchema.js │ ├── PluginSchema.js │ ├── SectionSchema.js │ ├── SiteSchema.js │ ├── UserGroupSchema.js │ └── UserSchema.js └── utils │ ├── FlintPlugin.js │ ├── collect-data.js │ ├── compile-sass.js │ ├── compile.js │ ├── create-admin-usergroup.js │ ├── database.js │ ├── emails │ ├── compile.js │ ├── flintlogo.png │ ├── index.js │ ├── sendEmail.js │ └── templates │ │ ├── forgot-password.html │ │ ├── layouts │ │ └── base.html │ │ ├── new-account.html │ │ ├── reset-password.html │ │ └── styles │ │ ├── _settings.scss │ │ ├── emails.scss │ │ └── foundation │ │ ├── _foundation.scss │ │ ├── _global.scss │ │ ├── components │ │ ├── _alignment.scss │ │ ├── _button.scss │ │ ├── _callout.scss │ │ ├── _code.scss │ │ ├── _media-query.scss │ │ ├── _menu.scss │ │ ├── _normalize.scss │ │ ├── _outlook-first.scss │ │ ├── _thumbnail.scss │ │ ├── _typography.scss │ │ └── _visibility.scss │ │ ├── grid │ │ ├── _block-grid.scss │ │ └── _grid.scss │ │ ├── settings │ │ └── _settings.scss │ │ └── util │ │ └── _util.scss │ ├── emit-socket-event.js │ ├── events.js │ ├── four-oh-four-handler.js │ ├── generate-env-file.js │ ├── get-asset-details.js │ ├── get-entry-data.js │ ├── get-user-permissions.js │ ├── handle-compile-error-routes.js │ ├── helpers.js │ ├── log.js │ ├── logger.js │ ├── nunjucks.js │ ├── passport.js │ ├── permissions.json │ ├── public-registration.js │ ├── reduce-perms-to-object.js │ ├── register-plugins.js │ ├── scaffold.js │ ├── template-routes.js │ ├── update-site-config.js │ └── validate-env-variables.js └── test ├── fixtures ├── images │ ├── image.png │ └── image2.png ├── logs │ ├── flint.log │ └── http-requests.log ├── plugins │ ├── ConsolePlugin.js │ └── icon.png ├── scss │ ├── main.css │ └── main.scss └── templates │ ├── 404.njk │ ├── 404.txt │ ├── empty │ └── .gitkeep │ ├── entry.njk │ ├── entry.txt │ ├── fieldFilter.njk │ ├── fieldFilter.txt │ ├── index.njk │ ├── index.txt │ ├── page-with-vars.njk │ └── page-with-vars.txt ├── index.test.js ├── mocks ├── assets.js ├── entries.js ├── fields.js ├── index.js ├── pages.js ├── plugins.js ├── sections.js ├── site.js ├── user.js ├── usergroups.js └── users.js ├── populatedb.js ├── server ├── apps │ ├── admin.test.js │ ├── common.js │ ├── first-run.test.js │ ├── plugins.test.js │ ├── routes │ │ ├── assets.test.js │ │ ├── auth.test.js │ │ ├── logs.test.js │ │ └── site.test.js │ └── types │ │ ├── assets.test.js │ │ ├── entries.test.js │ │ ├── fields.test.js │ │ ├── pages.test.js │ │ ├── sections.test.js │ │ ├── site.test.js │ │ ├── usergroups.test.js │ │ └── users.test.js └── utils │ ├── FlintPlugin.test.js │ ├── __snapshots__ │ └── compile.test.js.snap │ ├── compile-sass.test.js │ ├── compile.test.js │ ├── create-admin-usergroup.test.js │ ├── generate-env-file.test.js │ ├── get-entry-data.test.js │ ├── helpers.test.js │ ├── public-registration.test.js │ └── update-site-config.test.js └── setup.js /.env.dev: -------------------------------------------------------------------------------- 1 | # Secret settings 2 | SESSION_SECRET=Fy#xXd)L6UOjrJiOFCHpf3qqesa!h#+z 3 | 4 | # Mongo Credentials 5 | DB_HOST=127.0.0.1/test 6 | DB_USER=admin 7 | DB_PASS=admin 8 | 9 | DEBUG=flint* -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | admin 3 | public 4 | coverage -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["standard", "standard-jsx"], 4 | "rules": { 5 | "jsx-quotes": ["error", "prefer-double"], 6 | "react/jsx-no-bind": 0 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true, 11 | "experimentalObjectRestSpread": true 12 | } 13 | }, 14 | "settings": { 15 | "import/resolver": { 16 | "webpack": { 17 | "config": "config/webpack.config.js" 18 | } 19 | } 20 | }, 21 | "env": { 22 | "jest": true, 23 | "browser": true, 24 | "node": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /admin 3 | /templates 4 | /public 5 | /scss 6 | .env 7 | admin 8 | flint 9 | coverage 10 | public 11 | .nyc_output 12 | .DS_Store 13 | *.log 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | templates 2 | scss 3 | logs 4 | public 5 | dev.js 6 | test 7 | admin/report.html 8 | app 9 | .* 10 | coverage 11 | config 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | services: 10 | - mongodb 11 | 12 | node_js: 13 | - "8" 14 | 15 | before_script: 16 | - npm run build 17 | 18 | notifications: 19 | disabled: true 20 | 21 | install: 22 | - npm install -g codecov 23 | - npm install 24 | 25 | branches: 26 | except: 27 | - /^v\d+\.\d+\.\d+$/ 28 | 29 | script: 30 | - npm test 31 | - codecov 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jason Etcovitch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /app/actions/pluginActions.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_PLUGINS = 'REQUEST_PLUGINS' 2 | export const RECEIVE_PLUGINS = 'RECEIVE_PLUGINS' 3 | -------------------------------------------------------------------------------- /app/actions/siteActions.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import graphFetcher from '../utils/graphFetcher' 3 | import { newToast, errorToasts } from './uiActions' 4 | 5 | export const REQUEST_SITE = 'REQUEST_SITE' 6 | export const RECEIVE_SITE = 'RECEIVE_SITE' 7 | export const UPDATE_SITE = 'UPDATE_SITE' 8 | 9 | /** 10 | * Saves updates of the general site config 11 | * @param {Object} data 12 | */ 13 | export function updateSite (data) { 14 | return async (dispatch) => { 15 | const query = `mutation ($data: SiteInput!) { 16 | updateSite(data: $data) { 17 | siteUrl 18 | siteName 19 | defaultUserGroup 20 | allowPublicRegistration 21 | } 22 | }` 23 | 24 | return graphFetcher(query, { data }) 25 | .then((json) => { 26 | const { updateSite: updatedSite } = json.data.data 27 | dispatch({ type: UPDATE_SITE, updateSite: updatedSite }) 28 | dispatch(newToast({ 29 | message: The site configuration has been has been updated!, 30 | style: 'success' 31 | })) 32 | }) 33 | .catch(errorToasts) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/assets/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/android-chrome-256x256.png -------------------------------------------------------------------------------- /app/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /app/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #fe6300 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/assets/default_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/default_user.png -------------------------------------------------------------------------------- /app/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/favicon-16x16.png -------------------------------------------------------------------------------- /app/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/favicon-32x32.png -------------------------------------------------------------------------------- /app/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/favicon.png -------------------------------------------------------------------------------- /app/assets/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/mstile-150x150.png -------------------------------------------------------------------------------- /app/assets/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/components/Avatar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shape, string } from 'prop-types' 3 | import defaultImage from 'assets/default_user.png' 4 | 5 | export default function Avatar ({ user }) { 6 | if (user.image) return {user.username} 7 | return {user.username} 8 | } 9 | 10 | Avatar.propTypes = { 11 | user: shape({ 12 | username: string.isRequired, 13 | image: string 14 | }).isRequired 15 | } 16 | -------------------------------------------------------------------------------- /app/components/Breadcrumbs/Breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | .breadcrumbs { 2 | box-sizing: border-box; 3 | width: 100%; 4 | padding: 0.75rem 2rem; 5 | border-bottom: 1px solid $gray-300; 6 | flex-shrink: 0; 7 | background-color: $gray-000; 8 | font-size: 0.7rem; 9 | 10 | @include media($on-mobile) { 11 | padding: 0.7rem 1rem; 12 | } 13 | 14 | &__list { 15 | display: flex; 16 | list-style-type: none; 17 | padding: 0; 18 | margin: 0; 19 | 20 | &-item { 21 | &__separator { 22 | margin: 0 0.4em; 23 | color: $gray-500; 24 | } 25 | 26 | a { 27 | color: $gray-500; 28 | text-decoration: none; 29 | 30 | &:hover { color: $gray-700; } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/components/Breadcrumbs/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router-dom' 4 | import './Breadcrumbs.scss' 5 | 6 | export default class Breadcrumbs extends Component { 7 | static propTypes = { 8 | links: PropTypes.arrayOf(PropTypes.shape({ 9 | label: PropTypes.string.isRequired, 10 | path: PropTypes.string.isRequired 11 | })).isRequired 12 | }; 13 | 14 | render () { 15 | const { links } = this.props 16 | return ( 17 | 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/components/Button/Button.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | display: inline-block; 3 | padding: 0.6em; 4 | border: 1px solid $orange-dark; 5 | border-radius: 3px; 6 | background-color: $orange; 7 | background-image: linear-gradient(to top, rgba(white, 0), rgba(white, 0.1)); 8 | color: white; 9 | font-family: inherit; 10 | text-decoration: none; 11 | cursor: pointer; 12 | 13 | &:hover { background-color: $orange-light; } 14 | 15 | &:active { background-color: $orange-dark; } 16 | 17 | &:disabled { opacity: 0.5; pointer-events: none; } 18 | 19 | &--yes { background-color: $orange; } 20 | 21 | &--subtle { 22 | border-color: transparent; 23 | background-color: transparent; 24 | color: $gray-500; 25 | 26 | &:hover { 27 | border-color: $gray-300; 28 | background-color: $gray-300; 29 | } 30 | } 31 | 32 | &--small { font-size: 0.8rem; } 33 | } 34 | -------------------------------------------------------------------------------- /app/components/Checkbox/Checkbox.scss: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | &-wrapper { 3 | display: flex; 4 | align-items: center; 5 | } 6 | 7 | position: relative; 8 | width: 1em; 9 | height: 1em; 10 | border: 1px solid $gray-300; 11 | border-radius: 3px; 12 | background: none; 13 | transition: background-color 150ms $standard; 14 | 15 | &-group { 16 | .checkbox-wrapper + .checkbox-wrapper { margin-top: 0.4em; } 17 | 18 | &__label { 19 | display: block; 20 | margin-bottom: 0.4em; 21 | font-weight: 700; 22 | font-size: $size0; 23 | } 24 | } 25 | 26 | svg { 27 | position: absolute; 28 | top: 50%; 29 | left: 50%; 30 | fill: white; 31 | transform: translate(-50%, -50%) scale(0); 32 | transition: transform 101ms $exit; 33 | } 34 | 35 | &.is-checked { 36 | border-color: $gray-500; 37 | background-color: $orange; 38 | 39 | svg { 40 | transform: translate(-50%, -50%) scale(1); 41 | transition-duration: 200ms; 42 | transition-timing-function: $bounce; 43 | } 44 | } 45 | 46 | + .input__label { 47 | margin-left: 1em; 48 | margin-bottom: 0; 49 | font-weight: 500; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/components/Checkbox/Checkboxes.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Checkbox from './index' 4 | 5 | export default class Checkboxes extends Component { 6 | static propTypes = { 7 | name: PropTypes.string.isRequired, 8 | label: PropTypes.string.isRequired, 9 | instructions: PropTypes.string, 10 | checkboxes: PropTypes.arrayOf(PropTypes.object).isRequired 11 | } 12 | 13 | static defaultProps = { 14 | instructions: null 15 | } 16 | 17 | render () { 18 | const { checkboxes, name, label, instructions } = this.props 19 | 20 | return ( 21 |
22 | {label} 23 | {instructions &&

{instructions}

} 24 | {checkboxes.map(check => )} 25 |
26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/components/DatePicker/DayTile.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | const DayTile = ({ day, isActive, disabled, onClick }) => { 6 | const classes = classnames( 7 | 'datepicker__date', 8 | { 'is-active': isActive }, 9 | { 'is-disabled': disabled } 10 | ) 11 | 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | DayTile.propTypes = { 25 | day: PropTypes.number.isRequired, 26 | isActive: PropTypes.bool.isRequired, 27 | disabled: PropTypes.bool, 28 | onClick: PropTypes.func 29 | } 30 | 31 | DayTile.defaultProps = { 32 | disabled: false, 33 | onClick: null 34 | } 35 | 36 | export default DayTile 37 | -------------------------------------------------------------------------------- /app/components/DatePicker/Days.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 4 | 5 | const Days = () => ( 6 |
7 | {days.map(day => {day})} 8 |
) 9 | 10 | export default Days 11 | -------------------------------------------------------------------------------- /app/components/DeleteIcon/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { openModal } from 'actions/uiActions' 4 | import Icon from 'utils/icons' 5 | import store from 'utils/store' 6 | import ConfirmModal from '../Modals/ConfirmModal' 7 | 8 | export default class DeleteIcon extends Component { 9 | static propTypes = { 10 | onClick: PropTypes.func.isRequired, 11 | message: PropTypes.string, 12 | small: PropTypes.bool 13 | } 14 | 15 | static defaultProps = { 16 | message: undefined, 17 | small: false 18 | } 19 | 20 | constructor (props) { 21 | super(props) 22 | this.onClick = this.onClick.bind(this) 23 | } 24 | 25 | onClick () { 26 | const { onClick, message, small } = this.props 27 | store.dispatch(openModal( 28 | ) 33 | ) 34 | } 35 | 36 | render () { 37 | return ( 38 | 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/components/DropdownButton/DropdownButtons.scss: -------------------------------------------------------------------------------- 1 | .dropdown-btn { 2 | &__btn { 3 | position: relative; 4 | border-color: $orange-dark; 5 | background-color: $orange; 6 | 7 | &::before { 8 | content: ''; 9 | position: absolute; 10 | top: 0; 11 | right: 2em; 12 | width: 1px; 13 | height: 100%; 14 | background-color: $orange-light; 15 | } 16 | 17 | &::after { 18 | border-top-color: white; 19 | } 20 | } 21 | 22 | &__options { 23 | border-color: $orange-dark; 24 | background-color: $orange; 25 | } 26 | 27 | &__opt { 28 | display: block; 29 | box-sizing: border-box; 30 | padding: 0.6em; 31 | border: none; 32 | border-bottom: 1px solid $orange-dark; 33 | background: none; 34 | outline: none; 35 | background-color: $orange; 36 | color: white; 37 | text-decoration: none; 38 | 39 | &:last-of-type { border-bottom: none; } 40 | 41 | &:hover, &:focus { 42 | background-color: $orange-light; 43 | } 44 | } 45 | 46 | &.is-open { 47 | .dropdown-btn__btn { 48 | background-color: $orange-light; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/components/FieldOptions/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-array-index-key */ 2 | 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | import Fields from 'components/Fields' 6 | 7 | export default class FieldOptions extends Component { 8 | static propTypes = { 9 | fields: PropTypes.object, 10 | field: PropTypes.object, 11 | type: PropTypes.string.isRequired, 12 | onChange: PropTypes.func 13 | } 14 | 15 | static defaultProps = { 16 | fields: Fields, 17 | field: null, 18 | onChange: null 19 | } 20 | 21 | render () { 22 | const { fields, type, field, onChange } = this.props 23 | const optionFields = fields[type].fields 24 | 25 | if (!optionFields) return null 26 | 27 | return ( 28 |
29 |

{type} Options

30 | {(optionFields).map((F) => { 31 | if (!field) return 32 | return 33 | })} 34 |
35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/components/Fields/Asset/Asset.scss: -------------------------------------------------------------------------------- 1 | .asset { 2 | &__img-wrapper { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | width: 8rem; 7 | height: 8rem; 8 | padding: 1em; 9 | border-radius: 3px; 10 | border: 1px solid $gray-300; 11 | 12 | img { max-width: 100%; } 13 | } 14 | 15 | &__btn { 16 | padding: 0; 17 | border: none; 18 | margin: 0; 19 | background: white; 20 | cursor: pointer; 21 | 22 | @extend %focus; 23 | 24 | svg { 25 | fill: $gray-300; 26 | } 27 | 28 | &:hover { 29 | .asset__img-wrapper { 30 | border-color: $gray-500; 31 | } 32 | } 33 | 34 | &__title { 35 | box-sizing: border-box; 36 | width: 100%; 37 | border-radius: 3px; 38 | margin-top: 1em; 39 | margin-bottom: 0; 40 | font-size: $size0; 41 | font-weight: 500; 42 | } 43 | } 44 | 45 | &-modal { 46 | &__assets { 47 | display: flex; 48 | } 49 | 50 | .table-wrapper { 51 | width: 100%; 52 | 53 | .table__row:hover { 54 | background-color: $gray-100; 55 | cursor: pointer; 56 | } 57 | } 58 | } 59 | 60 | &__thumbnail { 61 | width: 4rem; 62 | height: 4rem; 63 | padding: 0; 64 | border: none; 65 | margin: 0; 66 | background: none; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/components/Fields/Color/Color.scss: -------------------------------------------------------------------------------- 1 | .color { 2 | &__btn { 3 | display: block; 4 | width: 60px; 5 | height: 2em; 6 | border-radius: 3px; 7 | border: 1px solid $gray-300; 8 | } 9 | 10 | &__overlay { 11 | position: fixed; 12 | top: 0; 13 | right: 0; 14 | bottom: 0; 15 | left: 0; 16 | } 17 | 18 | &__picker { 19 | position: absolute; 20 | z-index: 4; 21 | opacity: 0; 22 | visibility: hidden; 23 | transition: 24 | opacity 150ms $standard, 25 | visibility 150ms $standard; 26 | 27 | &.is-open { 28 | opacity: 1; 29 | visibility: visible; 30 | } 31 | 32 | .chrome-picker { font-family: inherit !important; } // stylelint-disable-line 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/components/Fields/Group/Group.scss: -------------------------------------------------------------------------------- 1 | .group { 2 | &__buttons { 3 | display: flex; 4 | font-size: $size0; 5 | 6 | &__btn { 7 | padding: 0.6em; 8 | border: 1px solid $gray-300; 9 | border-right-width: 0; 10 | background: white; 11 | font-family: inherit; 12 | cursor: pointer; 13 | 14 | &:hover { 15 | background-color: $gray-300; 16 | } 17 | 18 | &:first-of-type { 19 | border-top-left-radius: 3px; 20 | border-bottom-left-radius: 3px; 21 | 22 | &::before { 23 | content: '+ '; 24 | } 25 | } 26 | 27 | &:last-of-type { 28 | border-right-width: 1px; 29 | border-top-right-radius: 3px; 30 | border-bottom-right-radius: 3px; 31 | } 32 | } 33 | } 34 | 35 | &__block { 36 | position: relative; 37 | padding: 1em; 38 | border: 1px solid $gray-300; 39 | border-radius: 3px; 40 | background-color: $gray-100; 41 | 42 | &__btns { 43 | position: absolute; 44 | z-index: 2; 45 | top: 1em; 46 | right: 1em; 47 | display: flex; 48 | align-items: center; 49 | } 50 | } 51 | 52 | &__drag { 53 | display: flex; 54 | padding: 0; 55 | border: none; 56 | margin-right: 0.3em; 57 | background: none; 58 | cursor: pointer; 59 | 60 | svg { fill: $gray-400; } 61 | 62 | &:hover svg { fill: $gray-500; } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/components/Fields/Group/GroupTile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | import DeleteIcon from 'components/DeleteIcon' 6 | import store from 'utils/store' 7 | 8 | export default function GroupTile ({ onClick, label, handle, isActive, onDelete }) { 9 | const { dispatch } = store 10 | return ( 11 | 15 | {label} 16 | {handle && {handle}} 17 | {onDelete && onDelete(label)} dispatch={dispatch} />} 18 | 19 | ) 20 | } 21 | 22 | GroupTile.propTypes = { 23 | onClick: PropTypes.func.isRequired, 24 | label: PropTypes.string.isRequired, 25 | handle: PropTypes.string, 26 | isActive: PropTypes.bool.isRequired, 27 | onDelete: PropTypes.func 28 | } 29 | 30 | GroupTile.defaultProps = { 31 | onDelete: null, 32 | handle: null 33 | } 34 | -------------------------------------------------------------------------------- /app/components/Fields/RichText/RichText.scss: -------------------------------------------------------------------------------- 1 | .rich-text { 2 | &__tools { 3 | position: relative; 4 | display: flex; 5 | align-items: center; 6 | padding: 1em; 7 | border-top-left-radius: 3px; 8 | border-top-right-radius: 3px; 9 | border: 1px solid $gray-300; 10 | background-color: white; 11 | } 12 | 13 | &-editor { 14 | overflow-y: auto; 15 | max-height: 500px; 16 | font-family: $font; 17 | background-color: white; 18 | line-height: 1.5; 19 | 20 | @include scrollbar($gray-300, white, 8px); 21 | } 22 | 23 | &-toolbar { 24 | font-family: $font; 25 | 26 | button { 27 | border-color: $gray-300; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/components/Fields/Toggle/Toggle.scss: -------------------------------------------------------------------------------- 1 | .toggle { 2 | position: relative; 3 | width: 2em; 4 | height: 1em; 5 | padding: 0; 6 | border: 1px solid $gray-300; 7 | border-radius: calc(0.5em + 2px); 8 | margin: 0; 9 | background-color: white; 10 | outline: none; 11 | 12 | @extend %focus; 13 | 14 | &__marker { 15 | position: absolute; 16 | top: -0.2em; 17 | left: -0.1em; 18 | width: 1.2em; 19 | height: 1.2em; 20 | border-radius: 50%; 21 | border: 1px solid $gray-300; 22 | background-color: white; 23 | box-shadow: 0 2px 4px rgba(black, 0.1); 24 | transition: transform 150ms $bounce; 25 | } 26 | 27 | &-wrapper { 28 | &.is-active { 29 | .toggle { 30 | background-color: $orange; 31 | 32 | &__marker { 33 | transform: translateX(calc(100% - 0.2em)); 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/components/FileInput/FileInput.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/components/FileInput/FileInput.scss -------------------------------------------------------------------------------- /app/components/FlintLogo/FlintLogo.scss: -------------------------------------------------------------------------------- 1 | .flint-logo { 2 | &-wrapper { 3 | color: $gray-500; 4 | } 5 | 6 | &__poweredBy { 7 | margin: 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/components/Loading/Loading.scss: -------------------------------------------------------------------------------- 1 | .loading { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | height: 100%; 7 | background-color: $blurple; 8 | color: white; 9 | font-size: $size5; 10 | letter-spacing: -1px; 11 | } 12 | -------------------------------------------------------------------------------- /app/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './Loading.scss' 3 | 4 | export default class Loading extends Component { 5 | render () { 6 | return ( 7 |
Loading...
8 | ) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/components/Modals/ConfirmModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Button from '../Button' 4 | 5 | export default class ConfirmModal extends Component { 6 | static propTypes = { 7 | confirm: PropTypes.func.isRequired, 8 | close: PropTypes.func, 9 | message: PropTypes.string, 10 | small: PropTypes.bool 11 | } 12 | 13 | static defaultProps = { 14 | close: null, 15 | message: 'Are you sure you want to do this?', 16 | small: false 17 | } 18 | 19 | constructor (props) { 20 | super(props) 21 | this.confirm = this.confirm.bind(this) 22 | this.handleKeyPress = this.handleKeyPress.bind(this) 23 | } 24 | 25 | componentDidMount () { window.addEventListener('keyup', this.handleKeyPress) } 26 | componentWillUnmount () { window.removeEventListener('keyup', this.handleKeyPress) } 27 | 28 | handleKeyPress (e) { 29 | if (e.which === 13) this.confirm() 30 | } 31 | 32 | confirm () { 33 | this.props.confirm() 34 | this.props.close() 35 | } 36 | 37 | render () { 38 | const { close, message, small } = this.props 39 | 40 | return ( 41 |
42 | {message} 43 |
44 | 45 | 46 |
47 |
48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/components/Notification/Notification.scss: -------------------------------------------------------------------------------- 1 | .notification { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | padding: 1em; 6 | border-radius: 3px; 7 | font-size: $size0; 8 | 9 | svg { margin-right: 1em; flex-shrink: 0; } 10 | 11 | &--warning { background-color: $yellow; } 12 | 13 | &--error { 14 | background-color: $red; 15 | color: white; 16 | 17 | svg { fill: white; } 18 | } 19 | 20 | &--success { background-color: $green; } 21 | } 22 | -------------------------------------------------------------------------------- /app/components/Notification/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import Icon from 'utils/icons' 5 | import './Notification.scss' 6 | 7 | export default function Notification ({ type, children, formElement }) { 8 | const classes = classnames( 9 | 'notification', 10 | `notification--${type}`, 11 | { 'form-element': formElement } 12 | ) 13 | 14 | const icons = { 15 | warning: 'breakLink', 16 | error: 'cross', 17 | success: 'checkmark' 18 | } 19 | 20 | return ( 21 |
22 | 23 | {children} 24 |
25 | ) 26 | } 27 | 28 | Notification.propTypes = { 29 | type: PropTypes.oneOf(['warning', 'error', 'success']), 30 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, 31 | formElement: PropTypes.bool 32 | } 33 | 34 | Notification.defaultProps = { 35 | type: 'warning', 36 | formElement: true 37 | } 38 | -------------------------------------------------------------------------------- /app/components/SecondaryNav/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { NavLink } from 'react-router-dom' 4 | import './SecondaryNav.scss' 5 | 6 | const NavItem = props =>
  • {props.children}
  • 7 | const NavButton = props =>
  • 8 | 9 | NavItem.propTypes = { 10 | to: PropTypes.string.isRequired, 11 | children: PropTypes.string.isRequired 12 | } 13 | NavButton.propTypes = { 14 | onClick: PropTypes.func.isRequired, 15 | children: PropTypes.string.isRequired 16 | } 17 | 18 | export default class SecondaryNav extends Component { 19 | static propTypes = { 20 | links: PropTypes.arrayOf(PropTypes.shape({ 21 | label: PropTypes.string.isRequired, 22 | path: PropTypes.string, 23 | onClick: PropTypes.func 24 | })).isRequired, 25 | children: PropTypes.element 26 | } 27 | 28 | static defaultProps = { 29 | children: null 30 | } 31 | 32 | render () { 33 | const { links, children } = this.props 34 | 35 | return ( 36 | 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/components/StatusDot/StatusDot.scss: -------------------------------------------------------------------------------- 1 | .status { 2 | width: 0.8rem; 3 | height: 0.8rem; 4 | border-radius: 50%; 5 | border-width: 1px; 6 | border-style: solid; 7 | 8 | &--live { 9 | border-color: darken($green, 10%); 10 | background-color: $green; 11 | } 12 | 13 | &--draft { 14 | border-color: $orange-dark; 15 | background-color: $orange; 16 | } 17 | 18 | &--disabled { 19 | border-color: $gray-600; 20 | background-color: $gray-500; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/components/StatusDot/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { capitalize } from 'utils/helpers' 4 | import './StatusDot.scss' 5 | 6 | const StatusDot = ({ status }) =>
    7 | 8 | StatusDot.propTypes = { status: PropTypes.oneOf(['live', 'draft', 'disabled']) } 9 | StatusDot.defaultProps = { status: 'disabled' } 10 | 11 | export default StatusDot 12 | -------------------------------------------------------------------------------- /app/components/Table/Cell.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { truncate } from 'utils/helpers' 4 | 5 | const Cell = ({ column, children }) => ( 6 | 7 | {typeof children === 'string' ? truncate(children, 20) : children.component} 8 | 9 | ) 10 | 11 | Cell.propTypes = { 12 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 13 | column: PropTypes.string.isRequired 14 | } 15 | Cell.defaultProps = { children: '-' } 16 | 17 | export default Cell 18 | -------------------------------------------------------------------------------- /app/components/Table/THead.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import p from 'utils/prettyNames' 5 | import { truncate } from 'utils/helpers' 6 | 7 | const THead = ({ sortBy, column, direction, has, onClick, shouldTruncate }) => { 8 | const btnClass = classnames( 9 | 'table__header__btn', 10 | { 'is-active': sortBy === column }, 11 | { desc: sortBy === column && direction === 'DESC' }, 12 | { asc: sortBy === column && direction === 'ASC' } 13 | ) 14 | 15 | const label = p[column] || column 16 | 17 | if (has) return 18 | return ( 19 | 20 | 24 | 25 | ) 26 | } 27 | 28 | THead.propTypes = { 29 | sortBy: PropTypes.string.isRequired, 30 | column: PropTypes.string.isRequired, 31 | direction: PropTypes.oneOf(['ASC', 'DESC']).isRequired, 32 | has: PropTypes.bool.isRequired, 33 | onClick: PropTypes.func.isRequired, 34 | shouldTruncate: PropTypes.bool.isRequired 35 | } 36 | 37 | export default THead 38 | -------------------------------------------------------------------------------- /app/components/TitleBar/TitleBar.scss: -------------------------------------------------------------------------------- 1 | .title-bar { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | padding: 1em 2em; 6 | border-bottom: 1px solid $gray-300; 7 | flex-shrink: 0; 8 | background-color: $gray-000; 9 | 10 | @include media($on-mobile) { 11 | padding: 0.5em 1em; 12 | } 13 | 14 | &__title { 15 | margin: 1.18px 0; 16 | color: $orange; 17 | font-size: 1.6rem; 18 | font-weight: 500; 19 | 20 | @include media($on-mobile) { 21 | margin: 5px 0; 22 | font-size: 1.2rem; 23 | } 24 | } 25 | 26 | &__children { 27 | display: flex; 28 | 29 | .btn + .btn { margin-left: 0.5em; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/TitleBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { truncate, setTitle } from 'utils/helpers' 4 | import './TitleBar.scss' 5 | 6 | export default class TitleBar extends Component { 7 | static propTypes = { 8 | title: PropTypes.string.isRequired, 9 | setTitle: PropTypes.bool, 10 | children: PropTypes.any 11 | } 12 | 13 | static defaultProps = { 14 | children: null, 15 | setTitle: true 16 | } 17 | 18 | componentDidMount () { 19 | if (this.props.setTitle) setTitle(this.props.title) 20 | } 21 | componentWillUnmount () { 22 | if (this.props.setTitle) setTitle() 23 | } 24 | 25 | render () { 26 | const { title, children } = this.props 27 | return ( 28 |
    29 |

    {truncate(title, 40)}

    30 |
    31 | {children} 32 |
    33 |
    34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/components/Toast/Toast.scss: -------------------------------------------------------------------------------- 1 | .toasts { 2 | position: absolute; 3 | right: 0; 4 | bottom: 1em; 5 | overflow: hidden; 6 | padding-right: 0.8em; 7 | z-index: 9999; 8 | 9 | &__toast { 10 | display: flex; 11 | align-items: center; 12 | height: 3.4em; 13 | padding: 0 1em; 14 | border-radius: 3px; 15 | border: 1px solid $gray-900; 16 | background-color: $gray-700; 17 | color: white; 18 | font-size: $size0; 19 | box-shadow: $shadow1; 20 | transform: translateX(0); 21 | will-change: height; 22 | transition: 23 | height 200ms 300ms, 24 | transform 300ms, 25 | opacity 300ms; 26 | transition-timing-function: $enter; 27 | 28 | &--entering { transform: translateX(calc(100% + 0.6em)); } 29 | 30 | & + & { 31 | margin-top: 0.4em; 32 | } 33 | 34 | &--success { 35 | border-color: darken($green, 10%); 36 | background-color: $green; 37 | } 38 | 39 | &--error { 40 | border-color: darken($red, 10%); 41 | background-color: $red; 42 | } 43 | 44 | &--leaving { 45 | height: 0; 46 | opacity: 0; 47 | transform: translateX(calc(100% + 0.6em)); 48 | transition-timing-function: $exit; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/containers/Aside/Aside.scss: -------------------------------------------------------------------------------- 1 | .aside { 2 | align-self: flex-start; 3 | position: relative; 4 | box-sizing: border-box; 5 | width: 200px; 6 | padding: 2em; 7 | border-radius: 3px; 8 | border: 1px solid $gray-300; 9 | margin: 2em 2em 0 1em; 10 | background-color: $gray-000; 11 | 12 | @include media($on-mobile) { 13 | width: calc(100% - 2em); 14 | padding: 1em; 15 | margin: 1em; 16 | order: -1; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/containers/Empty/Empty.scss: -------------------------------------------------------------------------------- 1 | .empty { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | height: 100%; 7 | 8 | &__inner { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | line-height: 1.4; 14 | 15 | svg { 16 | margin-bottom: 1em; 17 | fill: $gray-500; 18 | } 19 | 20 | a { color: $orange; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/containers/Empty/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Icon from 'utils/icons' 4 | import './Empty.scss' 5 | 6 | export default class Empty extends Component { 7 | static propTypes = { children: PropTypes.any.isRequired } 8 | 9 | render () { 10 | const { children } = this.props 11 | 12 | return ( 13 |
    14 |
    15 | 16 | {children} 17 |
    18 |
    19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/containers/ErrorContainer/ErrorContainer.scss: -------------------------------------------------------------------------------- 1 | .error-page { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | height: 100%; 7 | 8 | &__inner { 9 | display: flex; 10 | align-items: center; 11 | padding: 2em; 12 | max-width: 300px; 13 | border-radius: 5px; 14 | border: 1px solid $gray-500; 15 | flex-direction: column; 16 | background-color: $gray-100; 17 | font-size: $size0; 18 | 19 | a { 20 | color: $orange; 21 | text-decoration: none; 22 | 23 | &:hover { 24 | text-decoration: underline; 25 | } 26 | } 27 | 28 | svg { fill: $gray-500; margin-bottom: 1em; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/containers/ErrorContainer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { getUrlParameter } from 'utils/helpers' 4 | import Icon from 'utils/icons' 5 | import './ErrorContainer.scss' 6 | 7 | export default class ErrorContainer extends Component { 8 | componentDidMount () { document.body.classList.add('body--grey') } 9 | componentWillUnmount () { document.body.classList.remove('body--grey') } 10 | 11 | render () { 12 | const reason = getUrlParameter('r') 13 | const page = getUrlParameter('p') 14 | const template = getUrlParameter('t') 15 | 16 | let str 17 | 18 | /* eslint-disable max-len */ 19 | switch (reason) { 20 | case 'no-template': 21 | str = The requested template {template}.njk was not found when the {page} page was requested. 22 | break 23 | case 'no-html': 24 | str = There was an error when compiling the template for the {page} page was requested. 25 | break 26 | case 'no-homepage': 27 | str = Your website does not have a homepage yet! You can create one by signing in to the admin dashboard. 28 | break 29 | default: 30 | str = Unknown error! 31 | } 32 | /* eslint-enable max-len */ 33 | 34 | return ( 35 |
    36 |
    37 | 38 | {str} 39 |
    40 |
    41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/containers/FieldLayout/FieldSource.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { DragSource } from 'react-dnd' 4 | import classnames from 'classnames' 5 | import source from './utils/source' 6 | import { collectSource } from './utils/collect' 7 | import c from './utils/constants' 8 | 9 | class FieldSource extends Component { 10 | static propTypes = { 11 | connectDragSource: PropTypes.func.isRequired, 12 | isDragging: PropTypes.bool.isRequired, 13 | field: PropTypes.object.isRequired, 14 | disabled: PropTypes.bool 15 | } 16 | 17 | static defaultProps = { 18 | disabled: false 19 | } 20 | 21 | render () { 22 | const { isDragging, connectDragSource, field, disabled } = this.props 23 | const { title } = field 24 | const classes = classnames( 25 | 'field-layout__fields__field', 26 | { 'is-disabled': disabled }, 27 | { 'is-dragging': isDragging } 28 | ) 29 | 30 | const comp = ( 31 |
    32 | {title} 33 |
    34 | ) 35 | 36 | if (disabled) return comp 37 | 38 | return connectDragSource(comp) 39 | } 40 | } 41 | 42 | export default DragSource(c.FIELD, source, collectSource)(FieldSource) 43 | -------------------------------------------------------------------------------- /app/containers/FieldLayout/FieldTarget.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { DropTarget } from 'react-dnd' 4 | import classnames from 'classnames' 5 | import target from './utils/target' 6 | import { collectTarget } from './utils/collect' 7 | import c from './utils/constants' 8 | import FieldTargetCard from './FieldTargetCard' 9 | 10 | class FieldTarget extends Component { 11 | static propTypes = { 12 | isOver: PropTypes.bool.isRequired, 13 | connectDropTarget: PropTypes.func.isRequired, 14 | removeField: PropTypes.func.isRequired, 15 | sortField: PropTypes.func.isRequired, 16 | canDrop: PropTypes.bool.isRequired, 17 | fields: PropTypes.array.isRequired 18 | } 19 | 20 | render () { 21 | const { 22 | isOver, 23 | canDrop, 24 | connectDropTarget, 25 | fields, 26 | removeField, 27 | sortField 28 | } = this.props 29 | 30 | const classes = classnames( 31 | 'field-layout__target', 32 | { 'can-drop': isOver && canDrop } 33 | ) 34 | 35 | return connectDropTarget( 36 | 47 | ) 48 | } 49 | } 50 | 51 | export default DropTarget(c.FIELD, target, collectTarget)(FieldTarget) 52 | -------------------------------------------------------------------------------- /app/containers/FieldLayout/utils/collect.js: -------------------------------------------------------------------------------- 1 | export function collectTarget (connect, monitor) { 2 | return { 3 | connectDropTarget: connect.dropTarget(), 4 | isOver: monitor.isOver(), 5 | isOverCurrent: monitor.isOver({ shallow: true }), 6 | canDrop: monitor.canDrop() 7 | } 8 | } 9 | 10 | export function collectSource (connect, monitor) { 11 | return { 12 | connectDragSource: connect.dragSource(), 13 | isDragging: monitor.isDragging() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/containers/FieldLayout/utils/constants.js: -------------------------------------------------------------------------------- 1 | const ItemTypes = { 2 | FIELD: 'field' 3 | } 4 | 5 | export default ItemTypes 6 | -------------------------------------------------------------------------------- /app/containers/FieldLayout/utils/source.js: -------------------------------------------------------------------------------- 1 | const fieldSource = { 2 | beginDrag (props) { 3 | return { 4 | index: props.index, 5 | field: props.field, 6 | isNew: props.isNew 7 | } 8 | } 9 | } 10 | 11 | export default fieldSource 12 | -------------------------------------------------------------------------------- /app/containers/FieldLayout/utils/target.js: -------------------------------------------------------------------------------- 1 | const target = { 2 | canDrop () { 3 | return true 4 | }, 5 | 6 | hover (props, monitor, component) { 7 | const item = monitor.getItem() 8 | if (!component.targ || item.isNew) return 9 | 10 | const dragIndex = item.index 11 | const hoverIndex = props.index 12 | 13 | if (dragIndex === hoverIndex) return 14 | 15 | const hoverBoundingRect = component.targ.getBoundingClientRect() 16 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 17 | const clientOffset = monitor.getClientOffset() 18 | const hoverClientY = clientOffset.y - hoverBoundingRect.top 19 | 20 | // Dragging downwards 21 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return 22 | 23 | // Dragging upwards 24 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return 25 | 26 | component.props.sortField(dragIndex, hoverIndex) 27 | 28 | item.index = hoverIndex // eslint-disable-line no-param-reassign 29 | }, 30 | 31 | drop (props, monitor, component) { 32 | const item = monitor.getItem() 33 | if (item.isNew && component.props.addField) { 34 | component.props.addField(item.field, monitor.getItem().index) 35 | } 36 | } 37 | } 38 | 39 | export default target 40 | -------------------------------------------------------------------------------- /app/containers/Footer/Footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 2em; 3 | border-top: 1px solid $gray-300; 4 | text-align: center; 5 | 6 | @include media($on-mobile) { 7 | padding: 1em; 8 | } 9 | 10 | &__made-with { 11 | color: $blurple; 12 | opacity: 0.5; 13 | filter: grayscale(100); 14 | font-size: 0.8rem; 15 | transition: filter 150ms $standard, opacity 150ms $standard; 16 | 17 | &:hover { 18 | opacity: 1; 19 | filter: none; 20 | } 21 | 22 | a { color: inherit; } 23 | } 24 | 25 | &__emoji { 26 | width: 1.6em; 27 | padding: 0; 28 | border: none; 29 | margin: 0; 30 | background: none; 31 | cursor: pointer; 32 | transition: transform 150ms $standard; 33 | outline: none; 34 | 35 | &:hover { transform: scale(1.2); } 36 | } 37 | 38 | .flint-logo { 39 | width: 80px; 40 | height: 40px; 41 | margin-top: 1em; 42 | fill: $gray-400; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/containers/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { shuffle } from 'utils/helpers' 3 | import FlintLogo from 'components/FlintLogo' 4 | import { version } from '../../../package.json' 5 | import './Footer.scss' 6 | 7 | const baseEmojis = ['🍍', '🕑', '🎏', '🔥', '🦄', '🍑', '🔑', '🙌', '❤️'] 8 | 9 | export default class Footer extends Component { 10 | state = { emojis: shuffle(baseEmojis), i: 0 } 11 | 12 | randomizeEmoji () { 13 | const { emojis, i } = this.state 14 | this.setState({ i: i < emojis.length - 1 ? i + 1 : 0 }) 15 | } 16 | 17 | render () { 18 | const { emojis, i } = this.state 19 | const btn = 20 | return ( 21 | 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/containers/LoginContainer/LoginContainer.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | display: flex; 3 | overflow-y: auto; 4 | width: 100%; 5 | height: 100%; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | 10 | .flint-logo-wrapper { 11 | margin-bottom: 2em; 12 | 13 | .flint-logo { fill: $gray-500; } 14 | } 15 | 16 | &__inner { 17 | @extend %card; 18 | width: 420px; 19 | max-width: 90%; 20 | } 21 | 22 | &__title { 23 | display: block; 24 | width: 100%; 25 | margin-bottom: 1em; 26 | font-size: $size2; 27 | text-align: center; 28 | } 29 | 30 | &__img { 31 | display: block; 32 | max-width: 420px; 33 | max-height: 200px; 34 | margin: 1em auto; 35 | } 36 | 37 | .input, .btn { 38 | display: block; 39 | width: 100%; 40 | } 41 | 42 | .btn { padding: 1em; } 43 | 44 | .input { margin-bottom: 1em; } 45 | 46 | &__forgot { 47 | margin-top: 1em; 48 | color: $gray-500; 49 | font-size: $size0; 50 | 51 | &:hover { 52 | color: $gray-700; 53 | } 54 | 55 | + .flint-logo-wrapper { 56 | margin-top: 2em; 57 | margin-bottom: 0; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/containers/LoginContainer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router-dom' 4 | import { get } from 'axios' 5 | import FlintLogo from 'components/FlintLogo' 6 | import './LoginContainer.scss' 7 | 8 | export default class LoginContainer extends Component { 9 | static propTypes = { 10 | children: PropTypes.object.isRequired, 11 | forgot: PropTypes.bool 12 | } 13 | 14 | static defaultProps = { 15 | forgot: true 16 | } 17 | 18 | state = { siteLogo: null, isFetching: true } 19 | 20 | componentWillMount () { document.body.classList.add('body--grey') } 21 | 22 | componentDidMount () { 23 | get('/admin/api/site').then(({ data }) => { 24 | this.setState({ siteLogo: data.siteLogo, isFetching: false }) 25 | }) 26 | } 27 | 28 | componentWillUnmount () { document.body.classList.remove('body--grey') } 29 | 30 | render () { 31 | const { siteLogo, isFetching } = this.state 32 | const { children, forgot } = this.props 33 | 34 | if (isFetching) return null 35 | 36 | return ( 37 |
    38 | {siteLogo ? {siteLogo.filename} : } 39 | {children} 40 | {forgot && Forgot your password?} 41 | {siteLogo && } 42 |
    43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/containers/Main/Main.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | overflow-y: auto; 4 | width: 100%; 5 | height: 100%; 6 | flex-wrap: wrap; 7 | } 8 | -------------------------------------------------------------------------------- /app/containers/Page/Page.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | display: flex; 3 | overflow-y: auto; 4 | width: calc(100% - 220px); 5 | min-height: 100%; 6 | flex-direction: column; 7 | flex-grow: 2; 8 | 9 | @include media($on-mobile) { 10 | width: 100%; 11 | flex: none; 12 | } 13 | 14 | .page__inner { 15 | box-sizing: border-box; 16 | padding: 2em; 17 | flex-grow: 2; 18 | 19 | @include media($on-mobile) { 20 | padding: 1em; 21 | } 22 | } 23 | } 24 | 25 | .content { 26 | display: flex; 27 | flex: 1 0 auto; 28 | 29 | @include media($on-mobile) { 30 | flex-direction: column; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/containers/Page/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import Breadcrumbs from 'components/Breadcrumbs' 5 | import Footer from 'containers/Footer' 6 | import './Page.scss' 7 | 8 | export default class Page extends Component { 9 | static propTypes = { 10 | children: PropTypes.oneOfType([ 11 | PropTypes.arrayOf(PropTypes.node), 12 | PropTypes.node 13 | ]).isRequired, 14 | links: PropTypes.arrayOf(PropTypes.shape({ 15 | label: PropTypes.string.isRequired, 16 | path: PropTypes.string.isRequired 17 | })), 18 | name: PropTypes.string.isRequired, 19 | onSubmit: PropTypes.func 20 | }; 21 | 22 | static defaultProps = { 23 | links: null, 24 | onSubmit: null 25 | }; 26 | 27 | render () { 28 | const { name, children, links, onSubmit } = this.props 29 | const classes = classnames( 30 | 'page', 31 | `page--${name}`, 32 | { 'page--form': onSubmit }, 33 | { 'has-breadcrumbs': links && links.length > 0 } 34 | ) 35 | 36 | if (onSubmit) { 37 | return ( 38 |
    { this.form = r }}> 39 | {links && } 40 | {children} 41 | 42 |