├── both ├── svg │ └── .gitkeep ├── components │ └── .gitkeep ├── global-constants.js ├── pages │ ├── FaqPage │ │ ├── constants.js │ │ ├── actions.js │ │ ├── reducer.js │ │ ├── index.js │ │ └── component.js │ ├── NotFoundPage │ │ └── index.js │ ├── ContactPage │ │ ├── index.js │ │ ├── component.js │ │ └── containers │ │ │ └── ContactForm │ │ │ └── index.js │ └── HomePage │ │ ├── index.js │ │ └── component.js ├── global-actions.js ├── util │ ├── environment-detection.js │ ├── constants.js │ ├── load-script.js │ ├── asset-urls.js │ ├── debounce.js │ ├── api-request.js │ ├── form-helpers.js │ ├── form-validation.js │ └── scrollIt.js ├── global │ ├── SiteBody │ │ └── index.js │ ├── Header │ │ ├── index.js │ │ └── containers │ │ │ └── SiteSearch │ │ │ └── index.js │ └── Footer │ │ └── index.js ├── containers │ ├── ErrorBoundary │ │ └── index.js │ └── AccordionGroup │ │ ├── index.js │ │ └── components │ │ └── Accordion │ │ └── index.js ├── inputs │ ├── InputError │ │ └── index.js │ ├── InputButtonCheckboxGroup │ │ ├── components │ │ │ └── InputButtonCheckbox │ │ │ │ └── index.js │ │ └── index.js │ ├── InputSelect │ │ └── index.js │ ├── InputDate │ │ └── index.js │ ├── InputTextarea │ │ └── index.js │ ├── InputTelephone │ │ └── index.js │ ├── InputSelectState │ │ └── index.js │ ├── InputMoney │ │ └── index.js │ ├── InputCheckbox │ │ └── index.js │ ├── InputText │ │ └── index.js │ ├── InputAddress │ │ └── index.js │ └── InputTypeahead │ │ └── index.js ├── website-main │ ├── reducers.js │ ├── app-constants.js │ ├── routes.js │ ├── app-actions.js │ ├── app.js │ └── app-reducer.js ├── global-reducer.js ├── blocks │ ├── RenderOnClientOnly │ │ └── index.js │ ├── WindowResizeTracker │ │ └── index.js │ └── LightBox │ │ └── index.js └── website-login │ └── app-login.js ├── public └── .gitkeep ├── scss ├── pages │ └── .gitkeep ├── components │ └── .gitkeep ├── global │ ├── _footer.scss │ ├── _header.scss │ └── _site-body.scss ├── site │ ├── _typography.scss │ └── _default.scss ├── config │ ├── _functional-helpers-config.scss │ ├── _breakpoints.scss │ ├── _reset.scss │ ├── _variables.scss │ ├── _atomic-config.scss │ └── _atomic-variables.scss ├── inputs │ └── _input-checkbox.scss └── site.scss ├── server ├── routes │ ├── redirect-data.js │ ├── api │ │ ├── login │ │ │ ├── logoutAction.js │ │ │ └── loginAction.js │ │ ├── admin-commands │ │ │ └── reloadAllCache.js │ │ ├── actions │ │ │ └── siteSearch.js │ │ ├── page │ │ │ └── pageData.js │ │ └── forms │ │ │ └── contact.js │ ├── view │ │ ├── redirect.js │ │ ├── login.js │ │ └── main.js │ ├── util │ │ └── auth.js │ ├── middleware.js │ └── index.js ├── views │ ├── email │ │ └── contact-email.js │ └── layout.js ├── data │ ├── faq.js │ ├── page-data.js │ └── index.js ├── models │ ├── Faq.js │ ├── pages │ │ ├── FaqPage.js │ │ ├── ContactPage.js │ │ └── HomePage.js │ ├── ContactEnquiry.js │ ├── User.js │ ├── SiteConfiguration.js │ └── MediaItem.js ├── cache │ ├── index.js │ ├── site-configuration.js │ └── site-search.js ├── updates │ └── 0.0.1-admin.js ├── cron │ └── index.js ├── util │ ├── fast-uuid.js │ └── send-email.js └── index.js ├── client ├── login.js └── main.js ├── .gitignore ├── .env.sample ├── .env.dev ├── .env.staged ├── .env.prod ├── package.json ├── webpack.config.js ├── WIRING-IN-PAGES.md └── README.md /both/svg/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scss/pages/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /both/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scss/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /both/global-constants.js: -------------------------------------------------------------------------------- 1 | export const WINDOW_RESIZED = 'global/WINDOW_RESIZED'; -------------------------------------------------------------------------------- /both/pages/FaqPage/constants.js: -------------------------------------------------------------------------------- 1 | export const GET_FAQS = 'app/FaqPage/GET_FAQS'; 2 | -------------------------------------------------------------------------------- /server/routes/redirect-data.js: -------------------------------------------------------------------------------- 1 | 2 | const redirectData = { 3 | // 'from': '/to', 4 | }; 5 | 6 | export default redirectData; 7 | -------------------------------------------------------------------------------- /both/pages/FaqPage/actions.js: -------------------------------------------------------------------------------- 1 | import { GET_FAQS } from './constants'; 2 | 3 | export const getFaqsAction = faqs => ({ 4 | type : GET_FAQS, 5 | faqs, 6 | }) 7 | -------------------------------------------------------------------------------- /server/routes/api/login/logoutAction.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = (req, res) => { 2 | req.session.destroy(err => err && console.warn('Problem destroying session: ', err)) 3 | res.json({ error : false }) 4 | } 5 | -------------------------------------------------------------------------------- /server/views/email/contact-email.js: -------------------------------------------------------------------------------- 1 | const contactEmail = content => ` 2 |
3 |
Subject: ${subject}


4 |
${message}
5 |
6 | `; 7 | 8 | export default contactEmail; 9 | -------------------------------------------------------------------------------- /server/routes/view/redirect.js: -------------------------------------------------------------------------------- 1 | import redirectData from '../redirect-data'; 2 | 3 | exports = module.exports = (request, response) => { 4 | const path = request.path.split('/')[1]; 5 | response.redirect(301, redirectData[path] || '/'); 6 | }; 7 | -------------------------------------------------------------------------------- /scss/global/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | height: $height-footer; 3 | 4 | @media all and (max-width: $bp-md) { 5 | height: $height-footer-md; 6 | } 7 | 8 | @media all and (max-width: $bp-sm) { 9 | height: $height-footer-sm; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /both/global-actions.js: -------------------------------------------------------------------------------- 1 | import { WINDOW_RESIZED } from './global-constants'; 2 | 3 | export const windowResizedAction = () => ({ 4 | type : WINDOW_RESIZED, 5 | windowSize : { 6 | width : window.innerWidth, 7 | height : window.innerHeight, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /scss/site/_typography.scss: -------------------------------------------------------------------------------- 1 | html { 2 | //set 10px baseline 3 | // the advantage of this is ems are easier to use 1.5em = 15px 4 | font-size: 10px; 5 | } 6 | 7 | body { 8 | font-family: sans-serif; 9 | font-size: $font-size-base; 10 | line-height: 1.5; 11 | } 12 | -------------------------------------------------------------------------------- /both/util/environment-detection.js: -------------------------------------------------------------------------------- 1 | // http://stackoverflow.com/questions/4224606/how-to-check-whether-a-script-is-running-under-node-js 2 | export const isBrowser = typeof window !== 'undefined' 3 | && ({}).toString.call(window) === '[object Window]'; 4 | 5 | export const isServer = !isBrowser; 6 | -------------------------------------------------------------------------------- /client/login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hydrate } from 'react-dom'; 3 | 4 | import AppLogin from '../both/website-login/app-login'; 5 | 6 | // bring in css 7 | import '../scss/site.scss'; 8 | 9 | hydrate( 10 | , 11 | document.getElementById('app') 12 | ); 13 | -------------------------------------------------------------------------------- /both/util/constants.js: -------------------------------------------------------------------------------- 1 | // breakpoint variables also defined _variables.scss 2 | export const bp = { 3 | lgx: 1500, 4 | lg : 1080, 5 | md : 820, 6 | sm : 650, 7 | smx: 500, 8 | }; 9 | 10 | // color variables also defined _variables.scss 11 | export const colors = { 12 | black : '#000000', 13 | '#000000' : 'black', 14 | }; 15 | -------------------------------------------------------------------------------- /scss/global/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | height: $height-header; 3 | 4 | @media all and (max-width: $bp-lg) { 5 | height: $height-header-md; 6 | } 7 | 8 | @media all and (max-width: $bp-md) { 9 | height: $height-header-md; 10 | } 11 | 12 | @media all and (max-width: $bp-sm) { 13 | height: $height-header-sm; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/routes/api/admin-commands/reloadAllCache.js: -------------------------------------------------------------------------------- 1 | import { initialCacheLoad } from '../../../cache'; 2 | 3 | exports = module.exports = (req, res) => { 4 | if(req.user && req.user.isAdmin) { 5 | initialCacheLoad(); 6 | res.json({ message: 'starting data load' }); 7 | } 8 | else 9 | res.json({ message: 'You must be logged in' }); 10 | } 11 | -------------------------------------------------------------------------------- /both/util/load-script.js: -------------------------------------------------------------------------------- 1 | export const loadScript = (id, src) => { 2 | const existingTag = document.getElementById(id); 3 | if(existingTag === null) { 4 | const script = document.createElement('script'); 5 | script.src = src; 6 | script.async = true; 7 | script.defer = true; 8 | script.id = id; 9 | document.body.appendChild(script); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/routes/api/login/loginAction.js: -------------------------------------------------------------------------------- 1 | const PASSWORD = process.env.PASSWORD || 'password'; 2 | 3 | exports = module.exports = (req, res) => { 4 | const { password } = req.body; 5 | if(password === PASSWORD) { 6 | req.session.loggedIn = true; 7 | res.json({ error : false }); 8 | } 9 | else 10 | res.json({ error : true , message : 'Incorrect password' }); 11 | } -------------------------------------------------------------------------------- /both/util/asset-urls.js: -------------------------------------------------------------------------------- 1 | import { isBrowser } from './environment-detection'; 2 | 3 | const env = isBrowser ? window.__ENV : process.env.NODE_ENV; 4 | const baseUrl = isBrowser ? window.__CLOUDFRONT_BASE_URL : process.env.CLOUDFRONT_BASE_URL; 5 | 6 | export const generateAssetUrl = (filename, path, asset) => 7 | `${baseUrl}${asset ? 'assets' : env}${path ? `/${path}/` : ''}${filename}` 8 | -------------------------------------------------------------------------------- /scss/config/_functional-helpers-config.scss: -------------------------------------------------------------------------------- 1 | $helper-bps: ( 2 | $bp-lgx-mx, 3 | $bp-lg-mx , 4 | $bp-md-mx , 5 | $bp-sm-mx , 6 | $bp-smx-mx, 7 | $bp-lgx-mn, 8 | $bp-lg-mn , 9 | $bp-md-mn , 10 | $bp-sm-mn , 11 | $bp-smx-mn, 12 | ); 13 | 14 | $centering-on: true; 15 | $display-toggles-on: true; 16 | $transforms-on: true; 17 | $transitions-on: true; 18 | $uncategorized-on: true; -------------------------------------------------------------------------------- /server/data/faq.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | 3 | export const getFaqsData = (/* ref */data) => new Promise((resolve, reject) => { 4 | keystone.list('Faq') 5 | .model 6 | .find() 7 | .exec((err, results) => { 8 | data.faqs = []; 9 | if(err) console.error(err); 10 | 11 | if(results) 12 | data.faqs = results; 13 | 14 | resolve(data); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /server/routes/api/actions/siteSearch.js: -------------------------------------------------------------------------------- 1 | import { siteSearchCache } from '../../../cache/site-search'; 2 | 3 | // You can populate this with any results you want to always return 4 | const defaultResults = []; 5 | 6 | exports = module.exports = (req, res) => { 7 | const { query } = req.body; 8 | 9 | const results = siteSearchCache.search(query); 10 | 11 | res.json({ results, defaultResults }); 12 | }; 13 | -------------------------------------------------------------------------------- /both/pages/FaqPage/reducer.js: -------------------------------------------------------------------------------- 1 | import { GET_FAQS } from './constants'; 2 | 3 | const initialState = { 4 | faqs : [], 5 | } 6 | 7 | export const faqPageReducer = (state = initialState, action) => { 8 | const newState = { ...state }; 9 | switch(action.type) { 10 | case GET_FAQS: 11 | newState.faqs = action.faqs; 12 | break; 13 | default: 14 | break; 15 | } 16 | return newState; 17 | } 18 | -------------------------------------------------------------------------------- /server/routes/api/page/pageData.js: -------------------------------------------------------------------------------- 1 | import populateData from '../../../data'; 2 | 3 | exports = module.exports = (req, res) => { 4 | // get the path as an array, ignoring the first two parts ('api' and 'page') 5 | let [_, __, pagePath, ...args] = req.path.split('/').splice(1); 6 | // populate the data using the same methods as a server side render 7 | populateData(pagePath, args, req, res) 8 | .then(data => res.json({ data })); 9 | }; -------------------------------------------------------------------------------- /both/global/SiteBody/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SiteBody = ({ className, children }) => ( 5 |
6 |
7 | {children} 8 |
9 |
10 | ); 11 | 12 | SiteBody.propTypes = { 13 | className : PropTypes.string, 14 | children : PropTypes.object.isRequired, 15 | }; 16 | 17 | export default SiteBody; 18 | -------------------------------------------------------------------------------- /both/containers/ErrorBoundary/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false } 7 | } 8 | 9 | componentDidCatch() { 10 | this.setState({ hasError: true }); 11 | } 12 | 13 | render() { 14 | return this.state.hasError 15 | ?
Error: try refreshing the page.
16 | : this.props.children; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /both/inputs/InputError/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const InputError = ({hasError, renderMessage, className}) => ( 5 |
6 | {hasError && renderMessage()} 7 |
8 | ); 9 | 10 | InputError.propTypes = { 11 | hasError : PropTypes.bool.isRequired, 12 | renderMessage : PropTypes.func.isRequired, 13 | className : PropTypes.string, 14 | } 15 | 16 | export default InputError; 17 | -------------------------------------------------------------------------------- /both/util/debounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Function} fn Function we are debouncing 3 | * @param {Number} interval How long to debounce at 4 | * @return {Function} Original function debounced 5 | */ 6 | function debounce(fn, interval) { 7 | let timer = null; 8 | return (...args) => { 9 | if(timer === null) { 10 | timer = setTimeout( 11 | () => (fn.call(this, ...args), timer = null), 12 | interval || 20); 13 | } 14 | } 15 | } 16 | 17 | export default debounce; 18 | -------------------------------------------------------------------------------- /both/website-main/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import { reducer as reduxAsyncConnect } from 'redux-connect'; 4 | 5 | import globalReducer from '../global-reducer'; 6 | import appReducer from './app-reducer'; 7 | import { faqPageReducer } from '../pages/FaqPage/reducer'; 8 | 9 | export default combineReducers({ 10 | globalReducer, 11 | appReducer, 12 | faqPageReducer, 13 | routing: routerReducer, 14 | reduxAsyncConnect,// must be last 15 | }); 16 | -------------------------------------------------------------------------------- /server/models/Faq.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | const Types = keystone.Field.Types; 3 | 4 | const Faq = new keystone.List('Faq', { 5 | map: { name: 'name' }, 6 | autokey: { path: 'slug', from: 'name', unique: true }, 7 | sortable: true, //.sort({'sortOrder': 1}) 8 | }); 9 | 10 | Faq.add({ 11 | name: { type: String, required: true }, 12 | question: { type: String, required: false }, 13 | answer: { type: Types.Html, wysiwyg: true }, 14 | }); 15 | 16 | Faq.defaultColumns = 'name, question, answer'; 17 | Faq.register(); 18 | -------------------------------------------------------------------------------- /scss/global/_site-body.scss: -------------------------------------------------------------------------------- 1 | .site-body { 2 | padding-top: $height-header; 3 | min-height: calc(100vh - #{$height-footer}); 4 | 5 | @media all and (max-width: $bp-md) { 6 | padding-top: $height-header-md; 7 | min-height: calc(100vh - #{$height-footer-md}); 8 | } 9 | 10 | @media all and (max-height: $height-cutoff-point) { 11 | min-height: $min-height-site; 12 | } 13 | 14 | @media all and (max-width: $bp-sm) { 15 | padding-top: $height-header-sm; 16 | min-height: calc(100vh - #{$height-footer-sm}); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /both/website-main/app-constants.js: -------------------------------------------------------------------------------- 1 | export const PAGE_DATA_LOAD_SUCCESS = 'global/PAGE_DATA_LOAD_SUCCESS'; 2 | export const PAGE_DATA_LOAD_FAILURE = 'global/PAGE_DATA_LOAD_FAILURE'; 3 | 4 | export const OPEN_NAVIGATION_SECTION = 'global/OPEN_NAVIGATION_SECTION'; 5 | export const OPEN_MOBILE_NAVIGATION = 'global/OPEN_MOBILE_NAVIGATION'; 6 | export const CLOSE_MOBILE_NAVIGATION = 'global/CLOSE_MOBILE_NAVIGATION'; 7 | 8 | export const OPEN_LIGHTBOX = 'global/OPEN_LIGHTBOX'; 9 | export const CLOSE_LIGHTBOX = 'global/CLOSE_LIGHTBOX'; 10 | -------------------------------------------------------------------------------- /server/models/pages/FaqPage.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | const Types = keystone.Field.Types; 3 | 4 | const FaqPage = new keystone.List('FaqPage', { 5 | map: { name: 'title' }, 6 | autokey: { path: 'slug', from: 'title', unique: true }, 7 | nocreate: !(process.env.NODE_ENV === 'dev' || process.env.CAN_CREATE_PAGES === 'true'), 8 | nodelete: true 9 | }); 10 | 11 | FaqPage.add({ 12 | title: { type: String, required: true }, 13 | meta: { type: String }, 14 | }); 15 | 16 | FaqPage.defaultColumns = 'title'; 17 | FaqPage.register(); 18 | -------------------------------------------------------------------------------- /both/util/api-request.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import { isServer } from './environment-detection'; 3 | 4 | export function apiRequest(apiPath, requestBody, data) { 5 | return (isServer) 6 | ? Promise.resolve({ data }) 7 | : fetch(`/api/${apiPath}/`, { 8 | credentials: 'include', 9 | method: 'POST', 10 | headers: { 11 | 'Accept' : 'application/json', 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify(requestBody), 15 | }) 16 | .then(r => r.json()); 17 | } 18 | -------------------------------------------------------------------------------- /scss/inputs/_input-checkbox.scss: -------------------------------------------------------------------------------- 1 | .input-checkbox { 2 | .checkbox__dummy--checked::before, 3 | .checkbox__dummy--checked::after { 4 | content: ""; 5 | position: absolute; 6 | height: 3px; 7 | background-color: $color-white; 8 | } 9 | 10 | .checkbox__dummy--checked::before { 11 | top: 9px; 12 | left: 1px; 13 | width: 7px; 14 | transform: rotateZ(45deg); 15 | } 16 | 17 | .checkbox__dummy--checked::after { 18 | top: 7px; 19 | left: 4px; 20 | width: 12px; 21 | transform: rotateZ(-45deg); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/models/pages/ContactPage.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | const Types = keystone.Field.Types; 3 | 4 | const ContactPage = new keystone.List('ContactPage', { 5 | map: { name: 'title' }, 6 | autokey: { path: 'slug', from: 'title', unique: true }, 7 | nocreate: !(process.env.NODE_ENV === 'dev' || process.env.CAN_CREATE_PAGES === 'true'), 8 | nodelete: true 9 | }); 10 | 11 | ContactPage.add({ 12 | title: { type: String, required: true }, 13 | meta: { type: String }, 14 | }); 15 | 16 | ContactPage.defaultColumns = 'title'; 17 | ContactPage.register(); 18 | -------------------------------------------------------------------------------- /both/pages/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | const NotFoundPage = () => ( 5 |
6 | 15 |
16 | Oh no! 404 Not Found 17 |
18 |
19 | ); 20 | 21 | export default NotFoundPage; 22 | -------------------------------------------------------------------------------- /server/models/ContactEnquiry.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | const Types = keystone.Field.Types; 3 | 4 | const ContactEnquiry = new keystone.List('ContactEnquiry', { 5 | map: { name: 'subject' }, 6 | autokey: { path: 'slug', from: 'subject', unique: true }, 7 | nocreate: (process.env.NODE_ENV !== 'dev'), 8 | noedit: true, 9 | }); 10 | 11 | ContactEnquiry.add({ 12 | subject : { type: String }, 13 | message : { type: Types.Textarea }, 14 | time : { type: String }, 15 | }); 16 | 17 | ContactEnquiry.defaultColumns = 'time, subject'; 18 | ContactEnquiry.register(); 19 | -------------------------------------------------------------------------------- /server/cache/index.js: -------------------------------------------------------------------------------- 1 | import { loadSiteConfiguration } from './site-configuration'; 2 | import { loadSiteSearch } from './site-search'; 3 | 4 | export const initialCacheLoad = () => new Promise((resolve, reject) => { 5 | 6 | console.log(new Date(), '**** Loading cache ****'); 7 | 8 | const promises = [ 9 | loadSiteSearch(), 10 | ]; 11 | 12 | loadSiteConfiguration() 13 | .then(() => { 14 | Promise.all(promises) 15 | .then(() => { 16 | console.log(new Date(), '**** Finished Loading cache ****'); 17 | resolve() 18 | }) 19 | .catch(reject); 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /server/data/page-data.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | 3 | export function getPageData( 4 | /* ref */data, 5 | modalName, 6 | populate, 7 | resultTransform = r => r 8 | ) { 9 | return new Promise((resolve, reject) => { 10 | let query = keystone.list(modalName) 11 | .model 12 | .findOne() 13 | 14 | if(populate) 15 | query = query.populate(populate); 16 | 17 | query.exec((err, result) => { 18 | if(err) console.warn("Error loading pages: ", err); 19 | data.pageData = resultTransform(result); 20 | resolve(data); 21 | }); 22 | }); 23 | } -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | const Types = keystone.Field.Types; 3 | 4 | const User = new keystone.List('User'); 5 | 6 | User.add({ 7 | name: { type: Types.Name, required: true, index: true }, 8 | email: { type: Types.Email, initial: true, required: true, index: true }, 9 | password: { type: Types.Password, initial: true, required: true }, 10 | }, 'Permissions', { 11 | isAdmin: { type: Boolean, label: 'Can access Keystone', index: true }, 12 | }); 13 | 14 | // Provide access to Keystone 15 | User.schema.virtual('canAccessKeystone').get(function () { 16 | return this.isAdmin; 17 | }); 18 | 19 | User.defaultColumns = 'name, email, isAdmin'; 20 | User.register(); 21 | -------------------------------------------------------------------------------- /both/global/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | import SiteSearch from './containers/SiteSearch'; 6 | 7 | const Header = ({ className }) => ( 8 |
9 |
10 | Home 11 | FAQs 12 | Contact 13 | 14 |
15 |
16 | ); 17 | 18 | Header.propTypes = { 19 | className : PropTypes.string, 20 | } 21 | 22 | export default Header; 23 | -------------------------------------------------------------------------------- /both/global-reducer.js: -------------------------------------------------------------------------------- 1 | import { WINDOW_RESIZED } from './global-constants'; 2 | 3 | import { isBrowser } from './util/environment-detection'; 4 | 5 | const initialState = { 6 | windowSize : { 7 | width : isBrowser ? window.innerWidth : 0, 8 | height : isBrowser ? window.innerHeight : 0, 9 | }, 10 | }; 11 | 12 | if(isBrowser) { 13 | window.addEventListener('resize', () => {}) 14 | } 15 | 16 | const reducer = (state = initialState, action) => { 17 | const newState = { ...state }; 18 | switch(action.type) { 19 | case WINDOW_RESIZED: 20 | newState.windowSize = action.windowSize; 21 | break; 22 | default: 23 | break; 24 | } 25 | return newState; 26 | } 27 | 28 | export default reducer; 29 | -------------------------------------------------------------------------------- /both/global/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import RenderOnClientOnly from '../../blocks/RenderOnClientOnly'; 5 | import { isBrowser } from '../../util/environment-detection'; 6 | 7 | const Footer = () => ( 8 | 23 | ); 24 | 25 | export default Footer; 26 | -------------------------------------------------------------------------------- /server/routes/util/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Produces a route authenticator that is either on or off based on the site's 3 | * configuration 4 | * @param {Bool} authenticationOn Is authentication on? 5 | * @return {func} An controller that either authenticates, or passes right through 6 | */ 7 | export const routeAuthFactory = authenticationOn => ( 8 | authenticationOn 9 | ? (controller, redirectPath) => (req, res) => { 10 | if(req.session.loggedIn) 11 | controller(req, res); 12 | else { 13 | if(redirectPath) 14 | res.redirect(301, redirectPath); 15 | else 16 | res.json({ error : true }); 17 | return; 18 | } 19 | } 20 | : controller => (req, res) => controller(req, res) 21 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # PEMS 11 | 12 | # Ignore mongo database files 13 | mongo_data 14 | mongo_data_staged 15 | 16 | # Ignoring webpack bundles 17 | public/*.css 18 | public/*.js 19 | public/*.map 20 | 21 | # Build directory for project 22 | build 23 | build-new 24 | build-bak 25 | build-staged 26 | 27 | # Dependency directory 28 | # Deployed apps should consider commenting this line out: 29 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 30 | node_modules 31 | 32 | # Ignore v8 build 33 | v8-compile-cache-0 34 | 35 | # Ignore .env configuration files/folders 36 | .env 37 | .env.tmp 38 | .vscode 39 | 40 | # Ignore .DS_Store files on OS X 41 | .DS_Store 42 | -------------------------------------------------------------------------------- /both/blocks/RenderOnClientOnly/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | 4 | export default class RenderOnClientOnly extends React.Component { 5 | constructor() { 6 | super(); 7 | this.state = { 8 | render: false 9 | } 10 | } 11 | 12 | static propTypes = { 13 | className : PropTypes.string, 14 | children : PropTypes.object.isRequired, 15 | } 16 | 17 | componentDidMount() { 18 | this.setState({ render: true }); 19 | } 20 | 21 | render() { 22 | const { className, children } = this.props; 23 | const { render } = this.state; 24 | return ( 25 |
26 | {render && children } 27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /both/website-main/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import App from './app'; 4 | import HomePage from '../pages/HomePage'; 5 | import FaqPage from '../pages/FaqPage'; 6 | import ContactPage from '../pages/ContactPage'; 7 | import NotFoundPage from '../pages/NotFoundPage'; 8 | 9 | const routes = [{ 10 | component: App, 11 | routes: [ 12 | { 13 | path : '/', 14 | exact : true, 15 | component: HomePage, 16 | }, 17 | { 18 | path : '/faqs', 19 | component: FaqPage, 20 | }, 21 | { 22 | path : '/contact', 23 | component: ContactPage, 24 | }, 25 | { 26 | path : '*', 27 | component: NotFoundPage, 28 | }, 29 | ], 30 | }]; 31 | 32 | export default routes; 33 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | NODE_ENV="dev" # dev, staged, production 2 | 3 | # Used for Keystone configuration 4 | COOKIE_SECRET="su9en9kgne2" 5 | MONGOOSE_ENCRIPTION_KEY=4tdxmve/04KTZipdZFvggNSd6GHUQMuAfpWegZbVp0w= 6 | MONGOOSE_SIGNING_KEY=4DilwnGwAzkkFUCipJTJ3yRbC1TWW6semuhD6qOo6IKULUzf/5lJUWXawQLkRhi9iSF6wMATSHlqlTAeZZ9uMQ== 7 | PORT=3000 8 | 9 | CAN_CREATE_PAGES=true 10 | 11 | # S3 Bucket credentials 12 | S3_BUCKET="" 13 | S3_KEY="" 14 | S3_SECRET="" 15 | S3_REGION="" 16 | S3_BASE_URL="//s3.amazonaws.com/bucket-name/" 17 | 18 | # Url to use for Amazon Cloudfront 19 | CLOUDFRONT_BASE_URL="//something.cloudfront.net/" 20 | 21 | # Configurations for sending email with Amazon SES 22 | SES_KEY="" 23 | SES_SECRET="" 24 | SES_REGION="" 25 | SES_TEST_TO_EMAIL="email@test.com" 26 | SES_TEST_BCC_EMAIL="email@test.com" 27 | -------------------------------------------------------------------------------- /server/updates/0.0.1-admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script automatically creates a default Admin user when an 3 | * empty database is used for the first time. You can use this 4 | * technique to insert data into any List you have defined. 5 | * 6 | * Alternatively, you can export a custom function for the update: 7 | * module.exports = function(done) { ... } 8 | */ 9 | 10 | exports.create = { 11 | User: [ 12 | { 'name.first': 'Admin', 'name.last': 'User', 'email': 'user@keystonejs.com', 'password': 'admin', 'isAdmin': true }, 13 | ], 14 | SiteConfiguration: [ 15 | { title : 'Global Site Configuration' }, 16 | ], 17 | HomePage: [ 18 | { title: 'Home Page' }, 19 | ], 20 | FaqPage: [ 21 | { title: 'Faq Page' }, 22 | ], 23 | ContactPage: [ 24 | { title: 'Contact Page' }, 25 | ], 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | NODE_ENV="dev" 2 | 3 | # Login for site 4 | SIMPLE_AUTH_ON=true 5 | PASSWORD=admin 6 | 7 | # Used for Keystone configuration 8 | COOKIE_SECRET="su9en9kgne2" 9 | MONGOOSE_ENCRIPTION_KEY=4tdxmve/04KTZipdZFvggNSd6GHUQMuAfpWegZbVp0w= 10 | MONGOOSE_SIGNING_KEY=4DilwnGwAzkkFUCipJTJ3yRbC1TWW6semuhD6qOo6IKULUzf/5lJUWXawQLkRhi9iSF6wMATSHlqlTAeZZ9uMQ== 11 | PORT=3000 12 | 13 | CAN_CREATE_PAGES=true 14 | 15 | # S3 Bucket credentials 16 | S3_BUCKET="" 17 | S3_KEY="" 18 | S3_SECRET="" 19 | S3_REGION="" 20 | S3_BASE_URL="//s3.amazonaws.com/bucket-name/" 21 | 22 | # Url to use for Amazon Cloudfront 23 | CLOUDFRONT_BASE_URL="//something.cloudfront.net/" 24 | 25 | # Configurations for sending email with Amazon SES 26 | SES_KEY="" 27 | SES_SECRET="" 28 | SES_REGION="" 29 | SES_TEST_TO_EMAIL="email@test.com" 30 | SES_TEST_BCC_EMAIL="email@test.com" 31 | -------------------------------------------------------------------------------- /server/cache/site-configuration.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | 3 | export const siteConfigurationCache = { 4 | lastLoaded: new Date(), 5 | data : {}, 6 | }; 7 | 8 | export const loadSiteConfiguration = next => new Promise((resolve, reject) => { 9 | console.log('*** Loading site configuration cache ***'); 10 | keystone.list('SiteConfiguration') 11 | .model 12 | .findOne() 13 | .populate('courseBrochure termsAndConditions') 14 | .exec((err, result) => { 15 | if(err) reject(); 16 | if(result) { 17 | siteConfigurationCache.lastLoaded = new Date(); 18 | siteConfigurationCache.data = result.toObject(); 19 | console.log('*** Finished loading site configuration cache ***'); 20 | resolve(siteConfigurationCache); 21 | } 22 | }); 23 | 24 | typeof(next) === 'function' && next(); 25 | }) 26 | -------------------------------------------------------------------------------- /server/routes/middleware.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import keystone from 'keystone'; 3 | import { siteConfigurationCache } from '../cache/site-configuration'; 4 | 5 | /** 6 | Initialises the standard view locals. 7 | Include anything that should be initialised before route controllers are executed. 8 | */ 9 | export const initLocals = (req, res, next) => { 10 | 11 | let locals = res.locals; 12 | 13 | // set if we are logged in as an admin 14 | locals.isAdmin = (req.user && req.user.isAdmin)|| false; 15 | 16 | // Add your own local variables here 17 | locals.siteConfig = siteConfigurationCache.data; 18 | 19 | // if we are on the staged site, then password protect site 20 | if(siteConfigurationCache.data.passwordProtected && !req.user) { 21 | res.redirect(301, '/keystone'); 22 | } 23 | 24 | next(); 25 | }; 26 | -------------------------------------------------------------------------------- /.env.staged: -------------------------------------------------------------------------------- 1 | NODE_ENV="staged" 2 | 3 | # Login for site 4 | SIMPLE_AUTH_ON=true 5 | PASSWORD=admin 6 | 7 | # Used for Keystone configuration 8 | COOKIE_SECRET="su9en9kgne2" 9 | MONGOOSE_ENCRIPTION_KEY=4tdxmve/04KTZipdZFvggNSd6GHUQMuAfpWegZbVp0w= 10 | MONGOOSE_SIGNING_KEY=4DilwnGwAzkkFUCipJTJ3yRbC1TWW6semuhD6qOo6IKULUzf/5lJUWXawQLkRhi9iSF6wMATSHlqlTAeZZ9uMQ== 11 | PORT=3000 12 | 13 | CAN_CREATE_PAGES=true 14 | 15 | # S3 Bucket credentials 16 | S3_BUCKET="" 17 | S3_KEY="" 18 | S3_SECRET="" 19 | S3_REGION="" 20 | S3_BASE_URL="//s3.amazonaws.com/bucket-name/" 21 | 22 | # Url to use for Amazon Cloudfront 23 | CLOUDFRONT_BASE_URL="//something.cloudfront.net/" 24 | 25 | # Configurations for sending email with Amazon SES 26 | SES_KEY="" 27 | SES_SECRET="" 28 | SES_REGION="" 29 | SES_TEST_TO_EMAIL="email@test.com" 30 | SES_TEST_BCC_EMAIL="email@test.com" 31 | 32 | -------------------------------------------------------------------------------- /server/models/SiteConfiguration.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | const Types = keystone.Field.Types; 3 | import { loadSiteConfiguration } from '../cache/site-configuration'; 4 | 5 | const SiteConfiguration = new keystone.List('SiteConfiguration', { 6 | map: { name: 'title' }, 7 | autokey: { path: 'slug', from: 'title', unique: true }, 8 | nocreate: (process.env.NODE_ENV !== 'dev'), 9 | nodelete: true 10 | }); 11 | 12 | SiteConfiguration.add({ 13 | title: { type: String, required: true }, 14 | passwordProtected: { type: Types.Boolean, label: 'Password Protected' }, 15 | }, 16 | 'Site Search', 17 | { 18 | searchFuzziness : { type: Number, note: 'Between 1 and 0' }, 19 | }); 20 | 21 | SiteConfiguration.schema.post('save', loadSiteConfiguration); 22 | 23 | SiteConfiguration.defaultColumns = 'title'; 24 | SiteConfiguration.register(); 25 | -------------------------------------------------------------------------------- /both/util/form-helpers.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Sets all fields to dirty 4 | * 5 | * @param {Object} initialDirty All the dirty flags so we can set them all false 6 | * @return {Object} An object containaining all dirty flags set to false 7 | */ 8 | export const getDirtyFields = initialDirty => 9 | Object.keys(initialDirty).reduce((acc, key) => acc = { ...acc, [key]: true}, {}) 10 | 11 | /** 12 | * Checks to see if there are errors in a form 13 | * 14 | * @param {Object} initialErrors All the errors, so we can look up what the current state is 15 | * @param {Object} state The state of the form 16 | * @return {Boolean} True if there is an error, false if there is no error 17 | */ 18 | export const formHasErrors = (initialErrors, state) => 19 | Object.keys(initialErrors).reduce((acc, key) => acc || state[key], false) 20 | -------------------------------------------------------------------------------- /server/routes/view/login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderToString } from 'react-dom/server'; 3 | import Helmet from 'react-helmet'; 4 | 5 | import AppLogin from '../../../both/website-login/app-login'; 6 | 7 | import renderLayout from '../../views/layout'; 8 | 9 | exports = module.exports = (request, response) => { 10 | 11 | // handle redirect if we aren't logged in 12 | if(request.session.loggedIn) { 13 | response.redirect(301, '/'); 14 | return; 15 | } 16 | 17 | // generate a string that we will render to the page 18 | const html = renderToString(); 19 | 20 | // get values for head: title, meta tags 21 | const head = Helmet.renderStatic(); 22 | 23 | // render the page, and send it to the client 24 | response.send(renderLayout(head, html, 'login', {}, !!(request.user && request.user.isAdmin))) 25 | }; 26 | -------------------------------------------------------------------------------- /both/util/form-validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Uses a regex to match email addresses 3 | * 4 | * @param {string} email 5 | * @return {boolean} true if valid, false if invalid 6 | */ 7 | export const validateEmail = email => { 8 | if(!email) return false; 9 | let pattern = /[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i 10 | return pattern.test(email); 11 | } 12 | 13 | export const isValidDate = d => d instanceof Date && !isNaN(d) 14 | 15 | // ToDo 16 | export const validateUsername = username => { 17 | return true; 18 | } 19 | 20 | // ToDo 21 | export const validatePassword = password => { 22 | return true; 23 | } 24 | 25 | // ToDo 26 | export const validatePhone = number => { 27 | if(!number) return false; 28 | number.replace(/(\d{3})\-?(\d{3})\-?(\d{4})/,'$1-$2-$3')) 29 | return pattern.test(number); 30 | } 31 | -------------------------------------------------------------------------------- /scss/config/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | $bp-lgx: 1700px; 2 | $bp-lg : 1097px; 3 | $bp-md : 784px; 4 | $bp-sm : 560px; 5 | $bp-smx: 400px; 6 | 7 | $bp-lgx-mx: (name: '\\@lgx', cond: 'all and (max-width: ' + $bp-lgx + ')'); 8 | $bp-lg-mx : (name: '\\@lg' , cond: 'all and (max-width: ' + $bp-lg + ')'); 9 | $bp-md-mx : (name: '\\@md' , cond: 'all and (max-width: ' + $bp-md + ')'); 10 | $bp-sm-mx : (name: '\\@sm' , cond: 'all and (max-width: ' + $bp-sm + ')'); 11 | $bp-smx-mx: (name: '\\@smx', cond: 'all and (max-width: ' + $bp-smx + ')'); 12 | 13 | $bp-lgx-mn: (name: '\\@lgx-mn', cond: 'all and (min-width: ' + $bp-lgx + ')'); 14 | $bp-lg-mn : (name: '\\@lg-mn', cond: 'all and (min-width: ' + $bp-lg + ')'); 15 | $bp-md-mn : (name: '\\@md-mn', cond: 'all and (min-width: ' + $bp-md + ')'); 16 | $bp-sm-mn : (name: '\\@sm-mn', cond: 'all and (min-width: ' + $bp-sm + ')'); 17 | $bp-smx-mn: (name: '\\@smx-mn', cond: 'all and (min-width: ' + $bp-smx + ')'); 18 | -------------------------------------------------------------------------------- /both/pages/ContactPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { asyncConnect } from 'redux-connect' 3 | import loadable from 'loadable-components' 4 | 5 | import { 6 | pageDataLoadSuccessAction, 7 | pageDataLoadFailureAction, 8 | } from '../../website-main/app-actions'; 9 | 10 | import { apiRequest } from '../../util/api-request'; 11 | 12 | const Page = loadable(() => 13 | import(/* webpackChunkName: "contact-page" */'./component') 14 | ) 15 | 16 | const mapStateToProps = state => ({ 17 | pageData : state.appReducer.pageData, 18 | }) 19 | 20 | @asyncConnect([{ 21 | promise: ({ params, helpers, store: { dispatch }, data }) => 22 | apiRequest('page/contact', {}, data) 23 | .then(({ data: { pageData } }) => dispatch(pageDataLoadSuccessAction(pageData))) 24 | }], mapStateToProps) 25 | export default class ContactPage extends React.Component { 26 | render() { 27 | return 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | NODE_ENV="production" 2 | 3 | # Login for site 4 | SIMPLE_AUTH_ON=true 5 | PASSWORD=admin 6 | 7 | # Used for Keystone configuration 8 | COOKIE_SECRET="su9en9kgne2" 9 | MONGOOSE_ENCRIPTION_KEY=4tdxmve/04KTZipdZFvggNSd6GHUQMuAfpWegZbVp0w= 10 | MONGOOSE_SIGNING_KEY=4DilwnGwAzkkFUCipJTJ3yRbC1TWW6semuhD6qOo6IKULUzf/5lJUWXawQLkRhi9iSF6wMATSHlqlTAeZZ9uMQ== 11 | PORT=3000 12 | 13 | CAN_CREATE_PAGES=false 14 | 15 | # S3 Bucket credentials 16 | S3_BUCKET="" 17 | S3_KEY="" 18 | S3_SECRET="" 19 | S3_REGION="" 20 | S3_BASE_URL="//s3.amazonaws.com/bucket-name/" 21 | 22 | # Url to use for Amazon Cloudfront 23 | CLOUDFRONT_BASE_URL="//something.cloudfront.net/" 24 | 25 | # Configurations for sending email with Amazon SES 26 | SES_KEY="" 27 | SES_SECRET="" 28 | SES_REGION="" 29 | SES_TEST_TO_EMAIL="email@test.com" 30 | SES_TEST_BCC_EMAIL="email@test.com" 31 | 32 | PORT=80 33 | SSL_KEY="/etc/letsencrypt/live/test.com/privkey.pem" 34 | SSL_CERT="/etc/letsencrypt/live/test.com/fullchain.pem" 35 | SSL_PORT=443 36 | -------------------------------------------------------------------------------- /both/pages/ContactPage/component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helmet from 'react-helmet'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | import ContactForm from './containers/ContactForm'; 7 | 8 | export default class ContactPage extends React.Component { 9 | 10 | static propTypes = { 11 | pageData: PropTypes.object, 12 | }; 13 | 14 | render() { 15 | const { pageData } = this.props; 16 | return pageData 17 | ? ( 18 |
19 | 28 |
29 | 30 |
31 |
32 | ) 33 | :
; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /both/website-main/app-actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | PAGE_DATA_LOAD_SUCCESS, 3 | PAGE_DATA_LOAD_FAILURE, 4 | OPEN_NAVIGATION_SECTION, 5 | OPEN_MOBILE_NAVIGATION, 6 | CLOSE_MOBILE_NAVIGATION, 7 | OPEN_LIGHTBOX, 8 | CLOSE_LIGHTBOX, 9 | } from './app-constants'; 10 | 11 | export const pageDataLoadSuccessAction = pageData => ({ 12 | type: PAGE_DATA_LOAD_SUCCESS, 13 | pageData, 14 | }) 15 | 16 | export const pageDataLoadFailureAction = pageData => ({ 17 | type: PAGE_DATA_LOAD_FAILURE, 18 | pageData, 19 | }) 20 | 21 | export const openNavSectionAction = section => ({ 22 | type: OPEN_NAVIGATION_SECTION, 23 | section, 24 | }) 25 | 26 | export const openMobileNavAction = () => ({ 27 | type: OPEN_MOBILE_NAVIGATION, 28 | }) 29 | 30 | export const closeMobileNavAction = () => ({ 31 | type: CLOSE_MOBILE_NAVIGATION, 32 | }) 33 | 34 | export const openLightboxAction = lightboxConfig => ({ 35 | type: OPEN_LIGHTBOX, 36 | lightboxConfig, 37 | }) 38 | 39 | export const closeLightboxAction = () => ({ 40 | type: CLOSE_LIGHTBOX, 41 | }) 42 | -------------------------------------------------------------------------------- /scss/site.scss: -------------------------------------------------------------------------------- 1 | @import "./config/variables"; 2 | @import "./config/breakpoints"; 3 | @import "./config/atomic-variables"; 4 | @import "./config/atomic-config"; 5 | @import "./config/functional-helpers-config"; 6 | 7 | @import "./config/reset"; 8 | @import "../node_modules/atomic-scss/scss/atomic"; 9 | @import "../node_modules/scss-functional-helpers/scss/functional-helpers"; 10 | 11 | // some default styling for elements 12 | @import "./site/default"; 13 | 14 | // icons 15 | // @import "./site/icons"; 16 | 17 | // site wide styles 18 | @import "./site/typography"; 19 | 20 | // styles for svgs. Since the css was written by illustrator, 21 | // it may make sense to keep them all in one place 22 | // @import "./site/svg"; 23 | 24 | // global components (used on every page) 25 | @import "./global/header"; 26 | @import "./global/site-body"; 27 | @import "./global/footer"; 28 | 29 | // blocks 30 | 31 | // components (used on some pages) 32 | 33 | // inputs 34 | @import "./inputs/input-checkbox"; 35 | 36 | // page level css (used on a single page) 37 | -------------------------------------------------------------------------------- /scss/site/_default.scss: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | -webkit-text-size-adjust: 100%; 7 | } 8 | 9 | body { 10 | background-color: $color-background; 11 | } 12 | 13 | .grid-container { 14 | max-width: $width-wrapper; 15 | margin: 0 auto; 16 | padding: 0 10px; 17 | } 18 | 19 | button { 20 | padding: 0; 21 | border: none; 22 | background: inherit; 23 | cursor: pointer; 24 | border-radius: 0; 25 | outline: none; 26 | } 27 | 28 | a { 29 | color: inherit; 30 | transition: color 0.3s, background 0.3s; 31 | } 32 | 33 | strong { 34 | font-weight: 700; 35 | } 36 | 37 | .user-entered-html { 38 | a { 39 | font-weight: 400; 40 | } 41 | 42 | strong { 43 | font-weight: 700; 44 | } 45 | 46 | em { 47 | font-style: italic; 48 | } 49 | 50 | ul, 51 | ol { 52 | padding-left: 20px; 53 | } 54 | 55 | ul { 56 | list-style-type: circle; 57 | } 58 | 59 | ol { 60 | list-style-type: decimal; 61 | } 62 | 63 | li { 64 | margin-bottom: 5px; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /both/blocks/WindowResizeTracker/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { debounce } from '../../util/debounce'; 6 | 7 | import { windowResizedAction } from '../../global-actions'; 8 | 9 | const mapDispatchToProps = dispatcher => ({ 10 | windowResized : () => dispatcher(windowResizedAction()), 11 | }) 12 | 13 | @connect(()=>({}), mapDispatchToProps) 14 | export default class WindowResizeTracker extends React.Component { 15 | static propTypes = { 16 | children : PropTypes.object.isRequired, 17 | // 18 | windowResized : PropTypes.func.isRequired, 19 | } 20 | 21 | resizeTrigger = ()=>{} 22 | 23 | componentDidMount() { 24 | this.windowResized = debounce(this.props.windowResized.bind(this), 100); 25 | window.addEventListener('resize', this.windowResized); 26 | } 27 | 28 | componentWillUnmount() { 29 | window.removeEventListener('resize', this.windowResized); 30 | } 31 | 32 | render() { 33 | 34 | { children } 35 | 36 | } 37 | } -------------------------------------------------------------------------------- /both/pages/FaqPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { asyncConnect } from 'redux-connect' 3 | import loadable from 'loadable-components' 4 | 5 | import { 6 | pageDataLoadSuccessAction, 7 | pageDataLoadFailureAction, 8 | } from '../../website-main/app-actions'; 9 | import { getFaqsAction } from './actions'; 10 | 11 | import { apiRequest } from '../../util/api-request'; 12 | 13 | const Page = loadable(() => 14 | import(/* webpackChunkName: "faq-page" */'./component') 15 | ) 16 | 17 | const mapStateToProps = state => ({ 18 | pageData : state.appReducer.pageData, 19 | faqs : state.faqPageReducer.faqs, 20 | }) 21 | 22 | @asyncConnect([{ 23 | promise: ({ params, helpers, store: { dispatch }, data }) => 24 | apiRequest('page/faqs', {}, data) 25 | .then(({ data: { pageData, faqs } }) => { 26 | dispatch(pageDataLoadSuccessAction(pageData)); 27 | dispatch(getFaqsAction(faqs)); 28 | }) 29 | }], mapStateToProps) 30 | export default class FaqPage extends React.Component { 31 | render() { 32 | return 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /both/blocks/LightBox/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * @param {Node} 6 | * @return {Boolean} 7 | */ 8 | const childClicked = target => target.classList.contains('light-box__children') 9 | 10 | const Lightbox = ({ className, lightboxConfig: { backgroundClassName, open, close, children }}) => ( 11 | open 12 | ?
13 |
17 | × 18 |
19 |
childClicked(e.target) && close()} 21 | className={`light-box__children posa center w100% h100% z0 ${backgroundClassName || ''}`} 22 | > 23 | { children } 24 |
25 |
26 | :
27 | ); 28 | 29 | Lightbox.propTypes = { 30 | className : PropTypes.string, 31 | lightboxConfig : PropTypes.object, // ToDo: describe shape? 32 | }; 33 | 34 | export default Lightbox; 35 | -------------------------------------------------------------------------------- /server/cron/index.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | 3 | const millisecondsUntilDate = d => d - new Date(); 4 | 5 | export const startCronJobs = () => { 6 | console.log("*** Starting Cron Jobs ***"); 7 | const chicagoTimezoneOffset = 300; 8 | const serverTimezoneOffset = (new Date()).getTimezoneOffset(); 9 | const timezoneAdjustment = chicagoTimezoneOffset - serverTimezoneOffset; 10 | let firstRun = true; 11 | 12 | // const someJob = () => { 13 | // const d = new Date(); 14 | // d.setHours(24, timezoneAdjustment, 0, 0); 15 | // const interval = millisecondsUntilDate(d); 16 | 17 | // keystone.list('Something') 18 | // .model 19 | // .findOne() 20 | // .exec((err, result) => { 21 | // if(err) console.error(err); 22 | // const runImmediately = !(!!result); 23 | 24 | // setTimeout(() => { 25 | // getCourseData() 26 | // .then(someJob); 27 | // }, interval); 28 | 29 | // if(runImmediately && firstRun) { 30 | // firstRun = false; 31 | // get 32 | // Data(); 33 | // } 34 | // }); 35 | // } 36 | 37 | // someJob(); 38 | } 39 | -------------------------------------------------------------------------------- /server/util/fast-uuid.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/coolaj86/7e5ebb9a6708d0ebfc78 2 | 3 | let crypto = require('crypto'); 4 | let pool = 31 * 128; // 36 chars minus 4 dashes and 1 four 5 | let r = crypto.randomBytes(pool); 6 | let j = 0; 7 | let str = "10000000-1000-4000-8000-100000000000"; 8 | let len = str.length; // 36 9 | let strs = []; 10 | 11 | strs.length = len; 12 | strs[8] = '-'; 13 | strs[13] = '-'; 14 | strs[18] = '-'; 15 | strs[23] = '-'; 16 | 17 | export const uuid = () => { 18 | let ch; 19 | let chi; 20 | 21 | for (chi = 0; chi < len; chi++) { 22 | ch = str[chi]; 23 | if ('-' === ch || '4' === ch) { 24 | strs[chi] = ch; 25 | continue; 26 | } 27 | 28 | // no idea why, but this is almost 4x slow if either 29 | // the increment is moved below or the >= is changed to > 30 | j++; 31 | if (j >= r.length) { 32 | r = crypto.randomBytes(pool); 33 | j = 0; 34 | } 35 | 36 | if ('8' === ch) { 37 | strs[chi] = (8 + r[j] % 4).toString(16); 38 | continue; 39 | } 40 | 41 | strs[chi] = (r[j] % 16).toString(16); 42 | } 43 | 44 | return strs.join(''); 45 | } 46 | -------------------------------------------------------------------------------- /server/util/send-email.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import aws from 'aws-sdk'; 3 | 4 | /** 5 | * Send an email with SES based on options passed in. 6 | * 7 | * @param {Object} emailOptions 'nodemailer' options 8 | * @return {Promise} 9 | */ 10 | const sendEmail = emailOptions => new Promise((resolve, reject) => { 11 | // create Nodemailer SES transporter 12 | const transporter = nodemailer.createTransport({ 13 | SES: new aws.SES({ 14 | apiVersion : '2010-12-01', 15 | accessKeyId : process.env.SES_KEY, 16 | secretAccessKey: process.env.SES_SECRET, 17 | region : process.env.SES_REGION, 18 | }) 19 | }); 20 | 21 | // send some mail 22 | transporter.sendMail(emailOptions, (err, info) => { 23 | const res = { success: true }; 24 | 25 | if(err) { 26 | console.error(err); 27 | res.success = false; 28 | reject(err); 29 | } 30 | 31 | if(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'staged') { 32 | console.log(info.envelope); 33 | console.log(info.messageId); 34 | } 35 | 36 | resolve(res); 37 | }); 38 | }); 39 | 40 | export default sendEmail; 41 | -------------------------------------------------------------------------------- /both/pages/HomePage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { asyncConnect } from 'redux-connect'; 3 | import loadable from 'loadable-components'; 4 | 5 | import { 6 | pageDataLoadSuccessAction, 7 | pageDataLoadFailureAction, 8 | openLightboxAction, 9 | closeLightboxAction, 10 | } from '../../website-main/app-actions'; 11 | 12 | import { apiRequest } from '../../util/api-request'; 13 | 14 | const Page = loadable(() => 15 | import(/* webpackChunkName: "home-page" */'./component') 16 | ) 17 | 18 | const mapStateToProps = state => ({ 19 | pageData : state.appReducer.pageData, 20 | }) 21 | 22 | const mapDispatchToProps = dispatch => ({ 23 | openLightbox : config => dispatch(openLightboxAction(config)), 24 | closeLightbox : () => dispatch(closeLightboxAction()), 25 | }) 26 | 27 | @asyncConnect([{ 28 | promise: ({ params, helpers, store: { dispatch }, data }) => 29 | apiRequest('page', {}, data) 30 | .then(({ data: { pageData } }) => dispatch(pageDataLoadSuccessAction(pageData))) 31 | }], mapStateToProps, mapDispatchToProps) 32 | export default class HomePage extends React.Component { 33 | render() { 34 | return 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hydrate } from 'react-dom'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { ReduxAsyncConnect } from 'redux-connect'; 6 | import { browserHistory } from 'react-router'; 7 | import createHistory from 'history/createBrowserHistory' 8 | import { ConnectedRouter, routerMiddleware } from 'react-router-redux'; 9 | 10 | import { setAppInitialState } from '../both/website-main/app-reducer'; 11 | import routes from '../both/website-main/routes'; 12 | import reducers from '../both/website-main/reducers'; 13 | 14 | // bring in css 15 | import '../scss/site.scss'; 16 | 17 | // This value is rendered into the DOM by the server 18 | const initialState = window.__INITIAL_STATE; 19 | 20 | // set the initial state of the global and app site configuration 21 | setAppInitialState(initialState.appReducer); 22 | 23 | const history = createHistory(); 24 | const middleware = routerMiddleware(history); 25 | 26 | // Create store with the initial state generated by the server 27 | const store = createStore(reducers, initialState, applyMiddleware(middleware)); 28 | 29 | hydrate( 30 | 31 | 32 | 33 | 34 | , 35 | document.getElementById('app') 36 | ); 37 | -------------------------------------------------------------------------------- /server/models/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | const Types = keystone.Field.Types; 3 | 4 | // const s3FilePath = `${process.env.NODE_ENV}/home-page/`; 5 | 6 | // const s3Storage = new keystone.Storage({ 7 | // adapter: require('keystone-storage-adapter-s3'), 8 | // s3: { 9 | // key: process.env.S3_KEY, 10 | // secret: process.env.S3_SECRET, 11 | // bucket: process.env.S3_BUCKET, 12 | // region: process.env.S3_REGION, 13 | // path: s3FilePath, 14 | // uploadParams: { 15 | // ACL: 'public-read', 16 | // }, 17 | // publicUrl : file => `https:${process.env.S3_BASE_URL}${s3FilePath}${file.filename}`, 18 | // schema: { 19 | // bucket: true, // optional; store the bucket the file was uploaded to in your db 20 | // etag: true, // optional; store the etag for the resource 21 | // path: true, // optional; store the path of the file in your db 22 | // url: true, // optional; generate & store a public URL 23 | // }, 24 | // }, 25 | // }); 26 | 27 | const HomePage = new keystone.List('HomePage', { 28 | map: { name: 'title' }, 29 | autokey: { path: 'slug', from: 'title', unique: true }, 30 | nocreate: !(process.env.NODE_ENV === 'dev' || process.env.CAN_CREATE_PAGES === 'true'), 31 | nodelete: true 32 | }); 33 | 34 | HomePage.add({ 35 | title: { type: String, required: true }, 36 | meta: { type: String }, 37 | }); 38 | 39 | HomePage.defaultColumns = 'title'; 40 | HomePage.register(); 41 | -------------------------------------------------------------------------------- /both/website-main/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { Link } from 'react-router-dom'; 5 | import renderRoutes from 'react-router-config/renderRoutes' 6 | 7 | import Lightbox from '../blocks/Lightbox'; 8 | import ErrorBoundary from '../containers/ErrorBoundary'; 9 | import Header from '../global/Header'; 10 | import Footer from '../global/Footer'; 11 | import SiteBody from '../global/SiteBody'; 12 | 13 | const mapStateToProps = state => ({ 14 | lightboxConfig: state.appReducer.lightboxConfig, 15 | }) 16 | 17 | @connect(mapStateToProps) 18 | export default class App extends React.Component { 19 | 20 | static propTypes = { 21 | route : PropTypes.object.isRequired, 22 | history : PropTypes.object.isRequired, 23 | lightboxConfig : PropTypes.object.isRequired, 24 | }; 25 | 26 | render() { 27 | const { route, history, lightboxConfig } = this.props; 28 | 29 | return( 30 | 31 |
32 |
33 | 34 | { renderRoutes(route.routes) } 35 | 36 |
37 | 41 |
42 |
43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /both/pages/FaqPage/component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helmet from 'react-helmet'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | import AccordionGroup from '../../containers/AccordionGroup'; 7 | 8 | export default class FaqPage extends React.Component { 9 | 10 | constructor(props) { 11 | super(); 12 | this.state = { 13 | faqs : props.faqs.map(({_id, question, answer }) => ({ 14 | id : _id, 15 | headingMessage : question, 16 | open : false, 17 | children : ( 18 |
19 |
20 |
21 | ), 22 | })), 23 | } 24 | } 25 | 26 | static propTypes = { 27 | pageData : PropTypes.object, 28 | faqs : PropTypes.array.isRequired, 29 | }; 30 | 31 | render() { 32 | const { pageData } = this.props; 33 | const { faqs } = this.state; 34 | return pageData 35 | ? ( 36 |
37 | 46 |
47 | 48 |
49 |
50 | ) 51 | :
; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/models/MediaItem.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | const Types = keystone.Field.Types; 3 | 4 | // const s3FilePath = `${process.env.NODE_ENV}/media-library/`; 5 | 6 | // const s3Storage = new keystone.Storage({ 7 | // adapter: require('keystone-storage-adapter-s3'), 8 | // s3: { 9 | // key: process.env.S3_KEY, 10 | // secret: process.env.S3_SECRET, 11 | // bucket: process.env.S3_BUCKET, 12 | // region: process.env.S3_REGION, 13 | // path: s3FilePath, 14 | // uploadParams: { 15 | // ACL: 'public-read', 16 | // }, 17 | // publicUrl : file => `https:${process.env.S3_BASE_URL}${s3FilePath}${file.filename}`, 18 | // generateFilename: file => file.originalname.replace(/ /g, '-'), 19 | // }, 20 | // schema: { 21 | // bucket: true, // optional; store the bucket the file was uploaded to in your db 22 | // etag: true, // optional; store the etag for the resource 23 | // path: true, // optional; store the path of the file in your db 24 | // url: true, // optional; generate & store a public URL 25 | // }, 26 | // }); 27 | 28 | const MediaItem = new keystone.List('MediaItem', { 29 | map: { name: 'name' }, 30 | autokey: { path: 'slug', from: 'name', unique: true }, 31 | }); 32 | 33 | MediaItem.add({ 34 | name: { type: String, required: true }, 35 | description: { type: String }, 36 | // media: { 37 | // type : Types.File, 38 | // storage: s3Storage, 39 | // createInline: true, 40 | // note : `Once uploaded, click the blue link above to view the uploaded item.`, 41 | // }, 42 | }); 43 | 44 | MediaItem.defaultColumns = 'name, media'; 45 | MediaItem.register(); 46 | -------------------------------------------------------------------------------- /both/util/scrollIt.js: -------------------------------------------------------------------------------- 1 | export const scrollIt = (destination, duration = 600, easing = 'linear', callback) => { 2 | 3 | const easings = { 4 | linear(t) { 5 | return t; 6 | } 7 | }; 8 | 9 | const start = window.pageYOffset; 10 | const startTime = 'now' in window.performance ? performance.now() : new Date().getTime(); 11 | 12 | const documentHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight); 13 | const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight; 14 | const destinationOffset = typeof destination === 'number' ? destination : destination.offsetTop; 15 | const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset); 16 | 17 | if ('requestAnimationFrame' in window === false) { 18 | window.scroll(0, destinationOffsetToScroll); 19 | if (callback) { 20 | callback(); 21 | } 22 | return; 23 | } 24 | 25 | function scroll() { 26 | const now = 'now' in window.performance ? performance.now() : new Date().getTime(); 27 | const time = Math.min(1, ((now - startTime) / duration)); 28 | window.scroll(0, Math.ceil((time * (destinationOffsetToScroll - start)) + start)); 29 | 30 | if (window.pageYOffset === destinationOffsetToScroll) { 31 | if (callback) { 32 | callback(); 33 | } 34 | return; 35 | } 36 | 37 | requestAnimationFrame(scroll); 38 | } 39 | 40 | scroll(); 41 | } 42 | -------------------------------------------------------------------------------- /scss/config/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | 5 | ++++++++++++Additions+++++++++++++++ 6 | 7 | - reset buttons to have no borders or background color 8 | - no 'x' for clearing field in IE 9 | */ 10 | html, body, div, span, applet, object, iframe, 11 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 12 | a, abbr, acronym, address, big, cite, code, 13 | del, dfn, em, img, ins, kbd, q, s, samp, 14 | small, strike, strong, sub, sup, tt, var, 15 | b, u, i, center, 16 | dl, dt, dd, ol, ul, li, 17 | fieldset, form, label, legend, 18 | table, caption, tbody, tfoot, thead, tr, th, td, 19 | article, aside, canvas, details, embed, 20 | figure, figcaption, footer, header, hgroup, 21 | menu, nav, output, ruby, section, summary, 22 | time, mark, audio, video { 23 | margin: 0; 24 | padding: 0; 25 | border: 0; 26 | font-size: 100%; 27 | font: inherit; 28 | vertical-align: baseline; 29 | } 30 | /* HTML5 display-role reset for older browsers */ 31 | article, aside, details, figcaption, figure, 32 | footer, header, hgroup, menu, nav, section { 33 | display: block; 34 | } 35 | body { 36 | line-height: 1; 37 | } 38 | ol, ul { 39 | list-style: none; 40 | } 41 | blockquote, q { 42 | quotes: none; 43 | } 44 | blockquote::before, blockquote::after, 45 | q::before, q::after { 46 | content: ''; 47 | content: none; 48 | } 49 | table { 50 | border-collapse: collapse; 51 | border-spacing: 0; 52 | } 53 | button { 54 | border: none; 55 | background: transparent; 56 | cursor: pointer; 57 | } 58 | 59 | // removes the 'x' clear button from inputs (IE/Edge) 60 | *::-ms-clear { display: none; } 61 | -------------------------------------------------------------------------------- /scss/config/_variables.scss: -------------------------------------------------------------------------------- 1 | //variable naming 2 | // use $-description: value 3 | // ex. 4 | // $color-light-blue 5 | // $width-large 6 | 7 | // base font -- assuming we are working with font-size: 10px; 8 | // default is 18px 9 | $font-size-base: 2rem; 10 | 11 | // wire framing colors 12 | $color-black : #101010; 13 | $color-white : #ffffff; 14 | $color-lightgray: lightgray; 15 | $color-gray : gray; 16 | 17 | // width of site's content 18 | $width-wrapper: 1024px; 19 | 20 | // the footer is always supposed to be on the bottom of the page, unless the 21 | // content naturally pushes the footer below the fold. However, some content 22 | // doesn't add to the document height, and there can be cases where the footer 23 | // will appear above other content when the browser has a small view height. 24 | // 25 | // When a browsers height is less than this variable, then we need to force the 26 | // footer below the bottom of the screen so everything will fit the following 27 | // pages/components determine this cutoff as necessary: 28 | // home page, navigation 29 | $height-cutoff-point: 750px; 30 | 31 | $padding-grid-side: 20px; 32 | $padding-grid-container: 0 $padding-grid-side; 33 | 34 | // heights/widths 35 | $height-header : 56px; 36 | $height-header-md: 48px; 37 | $height-header-sm: 48px; 38 | $height-footer : 208px; 39 | $height-footer-md: 208px; 40 | $height-footer-sm: 344px; 41 | 42 | $min-height-site : $height-cutoff-point; 43 | $min-height-site-sm: $height-cutoff-point + 400px; 44 | 45 | // site colors - often named using http://chir.ag/projects/name-that-color 46 | $color-background : $color-white; 47 | 48 | // transparent 49 | $color-black-transparent: rgba(#000, .25); 50 | 51 | //$color- #ce1a37; 52 | //$color- #db2c59; 53 | // nav colors purple, brass, dark-blue, bright-red, neon-yellow, almond, mid-blue 54 | -------------------------------------------------------------------------------- /both/inputs/InputButtonCheckboxGroup/components/InputButtonCheckbox/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class InputButtonCheckbox extends React.Component { 5 | 6 | static propTypes = { 7 | className : PropTypes.string, 8 | labelText : PropTypes.string, 9 | fieldName : PropTypes.string.isRequired, 10 | fieldValue : PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]).isRequired, 11 | fieldSelected : PropTypes.bool.isRequired, 12 | hasError : PropTypes.bool.isRequired, 13 | getFieldChanged: PropTypes.func.isRequired, 14 | }; 15 | 16 | render() { 17 | const { 18 | className, 19 | labelText, 20 | fieldName, 21 | fieldValue, 22 | fieldSelected, 23 | hasError, 24 | getFieldChanged, 25 | } = this.props; 26 | 27 | return ( 28 | 29 | this.checkbox = checkbox} 36 | onChange={ 37 | () => getFieldChanged({ 38 | fieldName, 39 | fieldSelected: !fieldSelected, 40 | }) 41 | } 42 | /> 43 | 44 | 50 | 51 | 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /both/inputs/InputSelect/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // this is a class so we can use ref 5 | export default class InputSelect extends React.Component { 6 | 7 | static propTypes = { 8 | fieldName : PropTypes.string.isRequired, 9 | fieldValue : PropTypes.oneOfType([ 10 | PropTypes.string, 11 | PropTypes.number]).isRequired, 12 | getFieldChanged : PropTypes.func.isRequired, 13 | setFieldDirty : PropTypes.func.isRequired, 14 | children : PropTypes.array.isRequired, 15 | hasError : PropTypes.bool.isRequired, 16 | disabled : PropTypes.bool, 17 | labelText : PropTypes.string, 18 | className : PropTypes.string, 19 | }; 20 | 21 | render() { 22 | const { 23 | fieldName, 24 | fieldValue, 25 | getFieldChanged, 26 | children, 27 | hasError, 28 | disabled, 29 | setFieldDirty, 30 | labelText, 31 | className, 32 | } = this.props; 33 | 34 | return ( 35 |
36 | {labelText && 37 | 40 | } 41 | 52 |
53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /both/pages/HomePage/component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helmet from 'react-helmet'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | import RenderOnClientOnly from '../../blocks/RenderOnClientOnly'; 7 | 8 | export default class HomePage extends React.Component { 9 | 10 | static propTypes = { 11 | pageData : PropTypes.object, 12 | openLightbox : PropTypes.func.isRequired, 13 | closeLightbox : PropTypes.func.isRequired, 14 | }; 15 | 16 | openLightbox = () => { 17 | const { openLightbox, closeLightbox } = this.props; 18 | const lightboxConfig = { 19 | close: closeLightbox, 20 | open: true, 21 | children: ( 22 |
23 | This is a lightbox. 24 |
25 | ), 26 | backgroundClassName: 'bgc-black-.5a', 27 | } 28 | openLightbox(lightboxConfig); 29 | } 30 | 31 | render() { 32 | const { pageData } = this.props; 33 | return pageData 34 | ? ( 35 |
36 | 45 |
46 |

47 | Hello World 48 |

49 | 50 |
This will only render on the client, and not on the server
51 |
52 |
53 |
54 | ) 55 | :
; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /both/website-main/app-reducer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | PAGE_DATA_LOAD_SUCCESS, 4 | PAGE_DATA_LOAD_FAILURE, 5 | OPEN_NAVIGATION_SECTION, 6 | OPEN_MOBILE_NAVIGATION, 7 | CLOSE_MOBILE_NAVIGATION, 8 | OPEN_LIGHTBOX, 9 | CLOSE_LIGHTBOX, 10 | } from './app-constants'; 11 | import { LOCATION_CHANGE } from 'react-router-redux'; 12 | 13 | let initialState = { 14 | pageData : {}, 15 | currentActiveSection : '', 16 | currentNavSection : '', 17 | mobileNavOpen : false, 18 | lightboxConfig : {}, 19 | }; 20 | 21 | // This is so we can load in the site configuration 22 | export const setAppInitialState = state => { 23 | initialState = { 24 | ...initialState, 25 | ...state, 26 | }; 27 | } 28 | 29 | const reducer = (state = initialState, action) => { 30 | const newState = { ...state }; 31 | switch(action.type) { 32 | //load page data 33 | case PAGE_DATA_LOAD_SUCCESS: 34 | newState.pageData = action.pageData; 35 | break; 36 | case PAGE_DATA_LOAD_FAILURE: 37 | console.warn('Problem loading page data'); 38 | break; 39 | 40 | // navigation state 41 | case OPEN_NAVIGATION_SECTION: 42 | newState.currentNavSection = action.section; 43 | break; 44 | case OPEN_MOBILE_NAVIGATION: 45 | newState.mobileNavOpen = true; 46 | break; 47 | case CLOSE_MOBILE_NAVIGATION: 48 | newState.mobileNavOpen = false; 49 | newState.currentNavSection = ''; 50 | break; 51 | 52 | // handle page routes 53 | case LOCATION_CHANGE: 54 | // always scroll top, and timeouts will move to proper place 55 | window.scrollTo(0, 0); 56 | break; 57 | case OPEN_LIGHTBOX: 58 | newState.lightboxConfig = action.lightboxConfig; 59 | break; 60 | case CLOSE_LIGHTBOX: 61 | newState.lightboxConfig = {}; 62 | break; 63 | default: break; 64 | } 65 | return newState; 66 | }; 67 | 68 | export default reducer; 69 | 70 | -------------------------------------------------------------------------------- /both/containers/AccordionGroup/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Accordion from './components/Accordion'; 4 | 5 | export default class AccordionGroup extends React.Component { 6 | 7 | constructor(props) { 8 | super(); 9 | const { accordionContent } = props; 10 | this.state = { 11 | accordions : accordionContent 12 | ? accordionContent.map(d => (d.open = d.open || false, d)) 13 | : [], 14 | }; 15 | } 16 | 17 | static propTypes = { 18 | className : PropTypes.string, 19 | accordionContent : PropTypes.arrayOf( 20 | PropTypes.shape({ 21 | id : PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]).isRequired, 22 | headingMessage : PropTypes.string.isRequired, 23 | open : PropTypes.bool.isRequired, 24 | children : PropTypes.object.isRequired, 25 | }) 26 | ).isRequired, 27 | viewOne : PropTypes.bool, 28 | }; 29 | 30 | toggleAccordion = accordion => { 31 | const { viewOne } = this.props; 32 | const accordions = (viewOne) 33 | ? this.state.accordions.map(acc => { 34 | acc.open = (acc.id === accordion.id) 35 | ? !acc.open 36 | : false; 37 | return acc; 38 | }) 39 | : this.state.accordions.map(acc => { 40 | acc.open = (acc.id === accordion.id) 41 | ? !acc.open 42 | : acc.open; 43 | return acc; 44 | }); 45 | 46 | this.setState({ accordions }); 47 | } 48 | 49 | render() { 50 | const { className } = this.props; 51 | const { accordions } = this.state; 52 | return ( 53 |
54 | { 55 | accordions.map((accordion, ndx) => 56 | 61 | ) 62 | } 63 |
64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /both/inputs/InputDate/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import InputText from '../InputText'; 4 | 5 | let lastSelectionStart = 0; 6 | let lastValue = ''; 7 | /** 8 | * Takes the value of a form input and fomats it to a date format 9 | * 10 | * @param {string} value The current value of the input 11 | * @return {string} New value for the input 12 | */ 13 | const formatDateField = (value, input) => { 14 | const { length } = value; 15 | const { selectionStart } = input; 16 | value = value.replace(/\D/g, ''); 17 | if(length > 5) 18 | value = `${value.substr(0,2)}/${value.substr(2,2)}/${value.substr(4,4)}`; 19 | else if(length > 3) 20 | value = `${value.substr(0,2)}/${value.substr(2,2)}`; 21 | else if(length === 1 && value[0] !== '0' && value[0] !== '1') 22 | value = `0${value}`; 23 | 24 | lastValue = value; 25 | lastSelectionStart = selectionStart 26 | return value; 27 | } 28 | 29 | /** 30 | * On blur, takes the value of a form input and fomats it to a date format 31 | * 32 | * @param {string} value The current value of the input 33 | * @return {string} New value for the input 34 | */ 35 | const formatOnBlur = value => { 36 | return value; 37 | } 38 | 39 | const InputDate = props =>( 40 |
41 | 46 |
47 | ); 48 | 49 | InputDate.propTypes = { 50 | className : PropTypes.string, 51 | outerClassName : PropTypes.string, 52 | fieldName : PropTypes.string.isRequired, 53 | fieldValue : PropTypes.string.isRequired, 54 | maxLength : PropTypes.number, 55 | getFieldChanged : PropTypes.func.isRequired, 56 | setFieldDirty : PropTypes.func.isRequired, 57 | labelText : PropTypes.string, 58 | hasError : PropTypes.bool, 59 | selectOnFocus : PropTypes.bool, 60 | onKeyUp : PropTypes.func, 61 | }; 62 | 63 | export default InputDate; 64 | -------------------------------------------------------------------------------- /server/cache/site-search.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | import Fuse from 'fuse.js'; 3 | 4 | import { siteConfigurationCache } from './site-configuration'; 5 | 6 | const fuseOptions = { 7 | shouldSort: true, 8 | threshold: 0.6, 9 | location: 0, 10 | distance: 1000, 11 | maxPatternLength: 32, 12 | minMatchCharLength: 1, 13 | keys: [ 14 | "name", 15 | "meta", 16 | "type" 17 | ] 18 | }; 19 | 20 | export const siteSearchCache = { 21 | lastLoaded : new Date(), 22 | data : null, 23 | search : null, 24 | }; 25 | 26 | const landingPagesToSearch = [ 27 | { 28 | model: 'HomePage', 29 | path: '/', 30 | }, 31 | { 32 | model: 'FaqPage', 33 | path: '/faqs', 34 | }, 35 | { 36 | model: 'ContactPage', 37 | path: '/contact', 38 | }, 39 | ]; 40 | 41 | export const loadSiteSearch = next => new Promise((resolve, reject) => { 42 | console.log("*** Initializing Site Search Cache ***"); 43 | 44 | fuseOptions.threshold = siteConfigurationCache.data.searchFuzziness || 0.6; 45 | 46 | siteSearchCache.lastLoaded = new Date(); 47 | siteSearchCache.data = []; 48 | 49 | const promises = landingPagesToSearch.map(({ model, path }) => new Promise((resolve, reject) => { 50 | keystone.list(model).model.findOne() 51 | .exec((err, result) => { 52 | if(err) reject(err); 53 | if(result) 54 | siteSearchCache.data.push({ 55 | name: result.title, 56 | meta: result.meta, 57 | path: path, 58 | type: 'landing-page', 59 | }); 60 | resolve(); 61 | }) 62 | })) 63 | 64 | Promise.all(promises) 65 | .then(() => { 66 | // create lookup object 67 | const fuse = new Fuse(siteSearchCache.data, fuseOptions); 68 | 69 | // bind the search method for fuse to cache object 70 | siteSearchCache.search = fuse.search.bind(fuse); 71 | 72 | console.log("*** Finished Loading Site Search Cache ***"); 73 | resolve(siteSearchCache); 74 | }) 75 | .catch(err => reject(err)) 76 | 77 | typeof(next) === 'function' && next(); 78 | }) 79 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | import redirectData from './redirect-data'; 3 | import { initLocals } from './middleware'; 4 | import { routeAuthFactory } from './util/auth'; 5 | 6 | const authOn = process.env.SIMPLE_AUTH_ON === 'true'; 7 | const authenticatedRoute = routeAuthFactory(authOn); 8 | 9 | const importRoutes = keystone.importer(__dirname); 10 | 11 | // Load Routes 12 | const controllers = { 13 | view : importRoutes('./view'), // view controllers 14 | /// 15 | /// APIs 16 | /// 17 | login : importRoutes('./api/login'), 18 | forms : importRoutes('./api/forms'), 19 | actions : importRoutes('./api/actions'), 20 | // general 21 | admin : importRoutes('./api/admin-commands'), 22 | page : importRoutes('./api/page'), 23 | }; 24 | 25 | // Bind Routes 26 | exports = module.exports = app => { 27 | 28 | // before any route, load in the locals 29 | keystone.pre('routes', initLocals); 30 | 31 | // redirects 32 | Object.keys(redirectData) 33 | .forEach((key) => app.get(`/${key}`, controllers.view.redirect)); 34 | 35 | /***************************************** 36 | ***************************************** 37 | API Routes 38 | ***************************************** 39 | *****************************************/ 40 | 41 | if(authOn) { 42 | app.post('/api/login', controllers.login.loginAction); 43 | app.all('/api/logout', controllers.login.logoutAction); 44 | 45 | // login page view route 46 | app.get('/login', controllers.view.login) 47 | } 48 | 49 | app.post('/api/contact', authenticatedRoute(controllers.forms.contact)); 50 | app.post('/api/site-search', authenticatedRoute(controllers.actions.siteSearch)); 51 | 52 | // Page API Routes 53 | app.all('/api/page', authenticatedRoute(controllers.page.pageData)); // home page on main site 54 | app.all('/api/page/*', authenticatedRoute(controllers.page.pageData)); // all other pages 55 | 56 | /// 57 | /// View routes 58 | /// 59 | 60 | // main site 61 | app.get('*', authenticatedRoute(controllers.view.main, '/login')); 62 | }; 63 | 64 | -------------------------------------------------------------------------------- /both/inputs/InputTextarea/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const noResize = { 5 | resize: 'none', 6 | }; 7 | 8 | export default class InputTextarea extends React.Component { 9 | 10 | static propTypes = { 11 | fieldName : PropTypes.string.isRequired, 12 | fieldValue : PropTypes.string.isRequired, 13 | getFieldChanged : PropTypes.func.isRequired, 14 | setFieldDirty : PropTypes.func.isRequired, 15 | maxCharacterCount: PropTypes.number, 16 | allowResize : PropTypes.bool, 17 | placeHolder : PropTypes.string, 18 | labelText : PropTypes.string, 19 | className : PropTypes.string, 20 | }; 21 | 22 | render() { 23 | const { 24 | fieldName, 25 | fieldValue, 26 | getFieldChanged, 27 | setFieldDirty, 28 | maxCharacterCount, 29 | allowResize, 30 | placeHolder, 31 | labelText, 32 | className, 33 | } = this.props; 34 | 35 | const textareaStyle = allowResize === false ? noResize : {}; 36 | 37 | return ( 38 |
39 | {labelText && 40 | 47 | } 48 | 60 | {maxCharacterCount && 61 |
62 | ({maxCharacterCount - fieldValue.length} characters left) 63 |
64 | } 65 |
66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/data/index.js: -------------------------------------------------------------------------------- 1 | // page data 2 | import { getPageData } from './page-data'; 3 | 4 | import { getFaqsData } from './faq'; 5 | 6 | // cache 7 | import { siteConfigurationCache } from '../cache/site-configuration'; 8 | 9 | /** 10 | * The function takes a url, and determines what functions will be required 11 | * for loading the required data out of mongo (from CMS data). 12 | * 13 | * Configuration is necessary if you add a new route, or need new data from mongo 14 | * 15 | * This could be abstracted higher, but right now it seems to be a good level of 16 | * abstraction for dealing with the project. 17 | * 18 | * @param {string} url the current route path we are working on 19 | * @return {Promise} A promise that will resolve with the data put together 20 | * from the required functions. 21 | */ 22 | export default function populateData(pagePath, args, req, res) { 23 | const data = {}; 24 | let [partOne] = req.path.split('/').splice(1); 25 | const isSSR = partOne !== 'api'; 26 | 27 | return new Promise((resolve, reject) => { 28 | const promises = handleMainPages(data, pagePath, args, req, res, isSSR); 29 | 30 | Promise.all(promises) 31 | .then(() => { 32 | if(data.redirect) 33 | res.redirect(302, data.redirect); 34 | resolve(data); 35 | }); 36 | }); 37 | } 38 | 39 | const handleMainPages = (/*ref*/data, pagePath, args, req, res, isSSR) => { 40 | const promises = []; 41 | 42 | // handle home page, which doesn't have a route 43 | if(pagePath === '/' || pagePath === '') 44 | pagePath = 'home-page'; 45 | 46 | // ******************** 47 | // these are all routes from the react-router configuration! 48 | // ******************** 49 | switch(pagePath) { 50 | case 'home-page': 51 | promises.push(getPageData(data, 'HomePage')); 52 | break; 53 | case 'faqs': 54 | promises.push(getPageData(data, 'FaqPage'), getFaqsData(data)); 55 | break; 56 | case 'contact': 57 | promises.push(getPageData(data, 'ContactPage')); 58 | break; 59 | default: 60 | break; 61 | } 62 | 63 | return promises; 64 | } 65 | -------------------------------------------------------------------------------- /both/website-login/app-login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fetch from 'isomorphic-fetch'; 3 | 4 | import InputText from '../inputs/InputText'; 5 | import InputError from '../inputs/InputError'; 6 | 7 | const initialValues = { passwordValue : '' }; 8 | const initialDirty = { passwordDirty : false }; 9 | const initialErrors = { passwordRequiredError : false }; 10 | 11 | export default class AppLogin extends React.Component { 12 | 13 | constructor() { 14 | super(); 15 | this.state = { 16 | ...initialValues, 17 | ...initialDirty, 18 | ...initialErrors, 19 | } 20 | } 21 | 22 | handleChanged = change => this.setState({ ...change }) 23 | 24 | handleSubmit = e => { 25 | e.preventDefault(); 26 | const { passwordValue } = this.state; 27 | const options = { 28 | credentials: 'include', 29 | method: 'POST', 30 | headers: { 31 | 'Accept' : 'application/json', 32 | 'Content-Type': 'application/json' 33 | }, 34 | body: JSON.stringify({ password : passwordValue }), 35 | }; 36 | 37 | fetch('/api/login', options) 38 | .then(r => r.json()) 39 | .then(data => { 40 | if(data.error) return; 41 | window.location.href = '/'; 42 | }) 43 | .catch(err => { 44 | reject(err); 45 | }) 46 | } 47 | 48 | render() { 49 | const { passwordValue, passwordRequiredError } = this.state; 50 | return( 51 |
52 |
53 | 62 | You must enter the correct password} 65 | /> 66 | 67 | 68 |
69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /both/inputs/InputTelephone/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import InputText from '../InputText'; 4 | 5 | let lastSelectionStart = 0; 6 | let lastValue = ''; 7 | /** 8 | * Takes the value of a form input and fomats it to a date format 9 | * 10 | * @param {string} value The current value of the input 11 | * @return {string} New value for the input 12 | */ 13 | const formatTelephoneField = (value, input) => { 14 | const { length } = value; 15 | const { selectionStart } = input; 16 | value = value.replace(/\D/g, ''); 17 | 18 | if(value.length === 6) 19 | value = `${value.substr(0,3)}-${value.substr(3,3)}`; 20 | else if(length > 6) 21 | value = `${value.substr(0,3)}-${value.substr(3,3)}-${value.substr(6,4)}`; 22 | else if(value.length === 3) 23 | value = value; 24 | else if(length > 3) 25 | value = `${value.substr(0,3)}-${value.substr(3,3)}`; 26 | 27 | lastValue = value; 28 | lastSelectionStart = selectionStart 29 | return value; 30 | } 31 | 32 | /** 33 | * On blur, takes the value of a form input and fomats it to a Telephone format 34 | * 35 | * @param {string} value The current value of the input 36 | * @return {string} New value for the input 37 | */ 38 | const formatOnBlur = value => { 39 | return value; 40 | } 41 | 42 | const InputTelephone = props =>( 43 |
44 | 49 |
50 | ); 51 | 52 | InputTelephone.propTypes = { 53 | className : PropTypes.string, 54 | outerClassName : PropTypes.string, 55 | fieldName : PropTypes.string.isRequired, 56 | fieldValue : PropTypes.string.isRequired, 57 | maxLength : PropTypes.number, 58 | getFieldChanged : PropTypes.func.isRequired, 59 | setFieldDirty : PropTypes.func.isRequired, 60 | labelText : PropTypes.string, 61 | hasError : PropTypes.bool, 62 | selectOnFocus : PropTypes.bool, 63 | onKeyUp : PropTypes.func, 64 | }; 65 | 66 | export default InputTelephone; 67 | -------------------------------------------------------------------------------- /both/inputs/InputSelectState/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import InputSelect from '../InputSelect'; 4 | 5 | const states = { 6 | AL: 'Alabama', 7 | AK: 'Alaska', 8 | AZ: 'Arizona ', 9 | AR: 'Arkansas', 10 | CA: 'California ', 11 | CO: 'Colorado', 12 | CT: 'Connecticut', 13 | DE: 'Delaware', 14 | FL: 'Florida', 15 | GA: 'Georgia', 16 | HI: 'Hawaii', 17 | ID: 'Idaho', 18 | IL: 'Illinois', 19 | IN: 'Indiana', 20 | IA: 'Iowa', 21 | KS: 'Kansas', 22 | KY: 'Kentucky', 23 | LA: 'Louisiana', 24 | ME: 'Maine', 25 | MD: 'Maryland', 26 | MA: 'Massachusetts', 27 | MI: 'Michigan', 28 | MN: 'Minnesota', 29 | MS: 'Mississippi', 30 | MO: 'Missouri', 31 | MT: 'Montana', 32 | NE: 'Nebraska', 33 | NV: 'Nevada', 34 | NH: 'New Hampshire', 35 | NJ: 'New Jersey', 36 | NM: 'New Mexico', 37 | NY: 'New York', 38 | NC: 'North Carolina', 39 | ND: 'North Dakota', 40 | OH: 'Ohio', 41 | OK: 'Oklahoma', 42 | OR: 'Oregon', 43 | PA: 'Pennsylvania', 44 | RI: 'Rhode Island', 45 | SC: 'South Carolina', 46 | SD: 'South Dakota', 47 | TN: 'Tennessee', 48 | TX: 'Texas', 49 | UT: 'Utah', 50 | VT: 'Vermont', 51 | VA: 'Virginia ', 52 | WA: 'Washington', 53 | WV: 'West Virginia', 54 | WI: 'Wisconsin', 55 | WY: 'Wyoming', 56 | } 57 | 58 | const InputSelectState = props => ( 59 | 60 | 61 | { 62 | Object.keys(states).map((key, i) => ( 63 | 69 | )) 70 | } 71 | 72 | ); 73 | 74 | InputSelectState.propTypes = { 75 | className : PropTypes.string, 76 | fieldName : PropTypes.string.isRequired, 77 | fieldValue : PropTypes.string.isRequired, 78 | setFieldDirty : PropTypes.func.isRequired, 79 | getFieldChanged : PropTypes.func.isRequired, 80 | hasError : PropTypes.bool.isRequired, 81 | hasError : PropTypes.bool.isRequired, 82 | abbrStateValue : PropTypes.bool, 83 | abbrStateDisplay : PropTypes.bool, 84 | }; 85 | 86 | export default InputSelectState; 87 | -------------------------------------------------------------------------------- /both/inputs/InputMoney/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import InputText from '../InputText'; 4 | 5 | /** 6 | * Takes the value of a form input and fomats it to a money format 7 | * 8 | * @param {string} value The current value of the input 9 | * @param {string} max The highest number value possible - will override value 10 | * @return {string} New value for the input 11 | */ 12 | const max = 25000; 13 | const formatDollarField = (value, input) => { 14 | const length = value.length; 15 | const dollarOnly = length === 1 && value === '$'; 16 | if(length === 0 || dollarOnly) 17 | return '$'; 18 | else { 19 | const vals = value.split('.'); 20 | if(vals.length > 1 && vals[1].length > 2) 21 | vals[1] = vals[1].substr(0,2); 22 | const endsInDot = vals.length > 1 && vals[1].length === 0; 23 | const decimalPlaces = vals.length > 1 ? vals[1].length : 0; 24 | value = vals.join('.'); 25 | let num = parseFloat(value[0] === '$' ? value.substr(1) : value, 10); 26 | if(num === Infinity || num > max) num = max; 27 | return (num === num) 28 | ? `$${num.toFixed(decimalPlaces)}${endsInDot ? '.' : ''}` 29 | : '$'; 30 | } 31 | } 32 | 33 | /** 34 | * On blur, takes the value of a form input and fomats it to a money format 35 | * 36 | * @param {string} value The current value of the input 37 | * @return {string} New value for the input 38 | */ 39 | const formatOnBlur = value => { 40 | const num = parseFloat(value.substr(1), 10); 41 | return num === num ? `$${num.toFixed(2)}` : '$0.00'; 42 | } 43 | 44 | const InputMoney = props =>( 45 |
46 | 51 |
52 | ); 53 | 54 | InputMoney.propTypes = { 55 | className : PropTypes.string, 56 | outerClassName : PropTypes.string, 57 | fieldName : PropTypes.string.isRequired, 58 | fieldValue : PropTypes.string.isRequired, 59 | getFieldChanged: PropTypes.func.isRequired, 60 | setFieldDirty : PropTypes.func.isRequired, 61 | labelText : PropTypes.string, 62 | hasError : PropTypes.bool, 63 | selectOnFocus : PropTypes.bool, 64 | onKeyUp : PropTypes.func, 65 | }; 66 | 67 | export default InputMoney; 68 | -------------------------------------------------------------------------------- /server/routes/api/forms/contact.js: -------------------------------------------------------------------------------- 1 | import keystone from 'keystone'; 2 | import sendEmail from '../../../util/send-email'; 3 | import contactEmail from '../../../views/email/contact-email'; 4 | 5 | exports = module.exports = (req, res) => { 6 | const { enquiry } = req.body; 7 | 8 | saveContactEnquiry(enquiry) 9 | .then(() => res.json({ error : false })) 10 | .catch(() => res.json({ false : true })); 11 | 12 | // Alternately, send an email - needs configuration for AWS SES 13 | // emailContactEnquiry(enquiry) 14 | // .then(() => res.json({ error : false })) 15 | // .catch(() => res.json({ error : true })); 16 | } 17 | 18 | /** 19 | * Write enquiry to database 20 | * 21 | * @param {string} subject Subject of the enquiry 22 | * @param {string} message Message of the enquiry 23 | */ 24 | const saveContactEnquiry = ({ subject, message }) => new Promise((resolve, reject) => { 25 | const ContactEnquiry = keystone.list('ContactEnquiry'); 26 | 27 | const enquiry = { 28 | subject, 29 | message, 30 | time : (new Date()).toUTCString(), 31 | }; 32 | 33 | const newEnquiry = new ContactEnquiry.model(enquiry); 34 | newEnquiry.save(); 35 | resolve(); 36 | }) 37 | 38 | /** 39 | * Send an email containing the enquery 40 | * 41 | * @param {string} subject Subject of the enquiry 42 | * @param {string} message Message of the enquiry 43 | */ 44 | const emailContactEnquiry = ({ subject, message }) => new Promise((resolve, reject) => { 45 | const html = contactEmail(subject, message); 46 | 47 | const mailOptions = ( 48 | process.env.NODE_ENV === 'production' ? 49 | { 50 | from : 'email@test.com', 51 | to : 'email@test.com', 52 | subject: 'A New Enquiry was Submitted', 53 | html, 54 | } 55 | : process.env.NODE_ENV === 'staged' ? 56 | { 57 | from : 'email@test.com', 58 | to : process.env.SES_TEST_TO_EMAIL, 59 | bcc : process.env.SES_TEST_BCC_EMAIL, 60 | subject: 'A New Enquiry was Submitted (Staged)', 61 | html, 62 | } 63 | : 64 | { 65 | from : 'email@test.com', 66 | to : process.env.SES_TEST_TO_EMAIL, 67 | bcc : process.env.SES_TEST_BCC_EMAIL, 68 | subject: 'A New Enquiry was Submitted (Dev)', 69 | html, 70 | } 71 | ); 72 | 73 | sendEmail(mailOptions) 74 | .then(resolve) 75 | .catch(err => reject(err)); 76 | }) 77 | -------------------------------------------------------------------------------- /both/inputs/InputCheckbox/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class InputCheckbox extends React.Component { 5 | 6 | static propTypes = { 7 | className : PropTypes.string, 8 | labelText : PropTypes.string, 9 | renderLabel : PropTypes.func, 10 | labelClassName : PropTypes.string, 11 | color : PropTypes.string, 12 | fieldName : PropTypes.string.isRequired, 13 | fieldValue : PropTypes.bool.isRequired, 14 | getFieldChanged: PropTypes.func.isRequired, 15 | setFieldDirty : PropTypes.func, 16 | resultValueKey : PropTypes.func, 17 | }; 18 | 19 | componentDidMount() { 20 | const { fieldName, getFieldChanged } = this.props; 21 | if(this.checkbox.checked) 22 | getFieldChanged({ 23 | [this.resultValueKeyFunction(fieldName)]: true, 24 | }); 25 | } 26 | 27 | // string -> string 28 | resultValueKeyFunction = fieldName => this.props.resultValueKey ? this.props.resultValueKey(fieldName) : `${fieldName}Value`; 29 | 30 | render() { 31 | const { 32 | className, 33 | labelText, 34 | renderLabel, 35 | color, 36 | fieldName, 37 | fieldValue, 38 | getFieldChanged, 39 | setFieldDirty, 40 | resultValueKey, 41 | } = this.props; 42 | 43 | return ( 44 | 45 | this.checkbox = checkbox} 51 | onChange={ 52 | () => { 53 | getFieldChanged({ 54 | [this.resultValueKeyFunction(fieldName)]: !fieldValue, 55 | }); 56 | setFieldDirty && setFieldDirty({[fieldName + 'Dirty']: true}) 57 | } 58 | } 59 | /> 60 | 71 | 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/routes/view/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | import { renderToString } from 'react-dom/server'; 5 | import StaticRouter from 'react-router/StaticRouter'; 6 | import { ReduxAsyncConnect, loadOnServer } from 'redux-connect'; 7 | import { parse as parseUrl } from 'url'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import routes from '../../../both/website-main/routes'; 11 | import reducers from '../../../both/website-main/reducers'; 12 | import { setAppInitialState } from '../../../both/website-main/app-reducer'; 13 | 14 | import renderLayout from '../../views/layout'; 15 | import populateData from '../../data'; 16 | 17 | exports = module.exports = (request, response) => { 18 | const url = request.originalUrl || request.url; 19 | const location = parseUrl(url); 20 | 21 | // load data out of keystone's interface to mongo 22 | let [pagePath, ...args] = request.path.split('/').splice(1); 23 | populateData(pagePath, args, request, response).then(data => { 24 | // get the site config out of locals, and initialize 25 | // the appReducer's initial state 26 | setAppInitialState(response.locals); 27 | 28 | // initialize a store for rendering app 29 | const store = createStore(reducers); 30 | 31 | // wait for all components to finish async requests 32 | loadOnServer({ location, routes, store, data }).then(() => { 33 | const context = {}; 34 | // generate a string that we will render to the page 35 | const html = renderToString( 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | 43 | // handle redirects 44 | if(context.url) { 45 | request.header('Location', context.url) 46 | return response.send(302) 47 | } 48 | 49 | // get values for head: title, meta tags 50 | const head = Helmet.renderStatic(); 51 | 52 | // render the page, and send it to the client 53 | response.send(renderLayout(head, html, 'main', store.getState(), !!(request.user && request.user.isAdmin))) 54 | 55 | }) 56 | .catch(err => { 57 | console.error(err); 58 | response.status(500).end(); 59 | }); 60 | }) 61 | .catch(err => { 62 | console.error(err); 63 | response.status(500).end(); 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /both/global/Header/containers/SiteSearch/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import fetch from 'isomorphic-fetch'; 4 | 5 | import InputText from '../../../../inputs/InputText'; 6 | import InputTypeahead from '../../../../inputs/InputTypeahead'; 7 | 8 | export default class SiteSearch extends React.Component { 9 | 10 | constructor() { 11 | super(); 12 | this.state = { 13 | searchResults : [], 14 | clearHandle : ()=>{}, 15 | }; 16 | } 17 | 18 | static contextTypes = { 19 | router: PropTypes.shape({ 20 | history: PropTypes.object.isRequired, 21 | }), 22 | }; 23 | 24 | /** 25 | * This gets a method from the inside of the typeahead that we can use to clear the input 26 | * 27 | * @param {function} clearHandle function we can call to clear the typeahead 28 | */ 29 | getClearHandle = clearHandle => this.setState({ clearHandle }); 30 | 31 | /** 32 | * Get the list of values from the typeahead. We need these to handle a keyUp 33 | * event for 34 | * 35 | * @param {Array} options.result the list of results 36 | */ 37 | handleSearchResultsChanged = ({ result }) => this.setState({ searchResults : result }) 38 | 39 | /** 40 | * When user presses enter, 'click' on the first entry 41 | * 42 | * @param {Object} e The keyUp event 43 | */ 44 | handleKeyUp = e => { 45 | const { router: { history } } = this.context; 46 | const { searchResults, clearHandle } = this.state; 47 | if(e.keyCode === 13 && searchResults.length > 0) { 48 | clearHandle(); 49 | history.push(searchResults[0].path); 50 | } 51 | } 52 | 53 | render() { 54 | const { className } = this.props; 55 | 56 | return ( 57 |
58 | {}} 62 | setFieldDirty={()=>{}} 63 | autocomplete="off" 64 | onKeyUp={this.handleKeyUp} 65 | debounceDelay={300} 66 | apiPath="/api/site-search" 67 | requestBodyKey="query" 68 | getClearHandle={this.getClearHandle} 69 | typeaheadResultCallback={this.handleSearchResultsChanged} 70 | responseLens={response => [ ...response.results, ...response.defaultResults ]} 71 | typeaheadResultsRenderer={result => { result.name }} 72 | /> 73 |
74 | ) 75 | } 76 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | // Require keystone 3 | import keystone from 'keystone'; 4 | import { startCronJobs } from './cron'; 5 | import { initialCacheLoad } from './cache'; 6 | 7 | global.__ENV = process.env.NODE_ENV; 8 | 9 | process.on('unhandledRejection', err => console.error('Unhandled rejection:', err)) 10 | 11 | let keystoneInit = { 12 | 'name': 'Keystone4 Universal React', 13 | 'brand': 'Keystone4 Universal React', 14 | 15 | 'static': '../public', 16 | 'mongo': process.env.MONGO_URI || 'mongodb://localhost/keystone4-universal-react', 17 | 18 | 'cookie secret': process.env.COOKIE_SECRET, 19 | 'auto update': true, 20 | 'session': true, 21 | 'session store': 'mongo', 22 | 'auth': true, 23 | 'user model': 'User', 24 | 'port': process.env.PORT || 3000, 25 | }; 26 | 27 | if(process.env.NODE_ENV === 'production') { 28 | // keystoneInit['ssl port'] = process.env.SSL_PORT || 443; 29 | // keystoneInit['ssl'] = 'force'; 30 | // keystoneInit['ssl key'] = process.env.SSL_KEY || '/etc/letsencrypt/live/test.org/privkey.pem'; 31 | // keystoneInit['ssl cert'] = process.env.SSL_CERT || '/etc/letsencrypt/live/test.org/fullchain.pem'; 32 | 33 | // This automatic letsencrypt isn't working right now for me 34 | // keystoneInit['letsencrypt'] = { 35 | // email: 'email@test.com', 36 | // domains: ['www.test.com', 'test.com'], 37 | // register: true, 38 | // tos: true, 39 | // }; 40 | } 41 | 42 | keystone.init(keystoneInit); 43 | 44 | // Load your project's Models 45 | keystone.import('models'); 46 | 47 | // Setup common locals for your templates. The following are required for the 48 | // bundled templates and layouts. Any runtime locals (that should be set uniquely 49 | // for each request) should be added to ./routes/middleware.js 50 | keystone.set('locals', { 51 | _: require('lodash'), 52 | env: keystone.get('env'), 53 | utils: keystone.utils, 54 | editable: keystone.content.editable, 55 | }); 56 | 57 | // Load your project's Routes 58 | keystone.set('routes', require('./routes')); 59 | 60 | // Configure the navigation bar in Keystone's Admin UI 61 | keystone.set('nav', { 62 | 'pages': [ 63 | 'home-pages', 64 | 'faq-pages', 65 | 'contact-pages', 66 | ], 67 | 'data' : [ 68 | 'faqs', 69 | 'media-items', 70 | 'contact-enquiries', 71 | ], 72 | 'configuration' : [ 'site-configurations' ], 73 | 'users' : [ 'users' ], 74 | }); 75 | 76 | // Start Keystone to connect to your database and initialise the web server 77 | keystone.start(); 78 | 79 | // build cache 80 | initialCacheLoad() 81 | .finally(() => { 82 | // start cron jobs 83 | startCronJobs(); 84 | }); 85 | -------------------------------------------------------------------------------- /both/inputs/InputButtonCheckboxGroup/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import InputButtonCheckbox from './components/InputButtonCheckbox'; 4 | 5 | export default class InputButtonCheckboxGroup extends React.Component { 6 | 7 | constructor(props) { 8 | super(); 9 | 10 | this.state = { 11 | buttons: props.fieldValue, 12 | }; 13 | } 14 | 15 | static propTypes = { 16 | className : PropTypes.string, 17 | buttonClassName: PropTypes.string, 18 | multiSelect : PropTypes.bool, 19 | fieldName : PropTypes.string.isRequired, 20 | getFieldChanged: PropTypes.func.isRequired, 21 | hasError : PropTypes.bool.isRequired, 22 | fieldValue : PropTypes.arrayOf(PropTypes.shape({ 23 | fieldName : PropTypes.string.isRequired, 24 | labelText : PropTypes.string.isRequired, 25 | fieldValue : PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]).isRequired, 26 | fieldSelected : PropTypes.bool.isRequired, 27 | className : PropTypes.string, 28 | })).isRequired, 29 | }; 30 | 31 | getFieldChanged = field => { 32 | const { fieldName, multiSelect } = this.props; 33 | // apply a different map depending on if we are multi-selecing or not 34 | const buttons = multiSelect 35 | ? this.state.buttons.map(b => b.fieldName === field.fieldName 36 | ? (b.fieldSelected = !b.fieldSelected, b) 37 | : b 38 | ) 39 | : this.state.buttons.map(b => b.fieldName === field.fieldName 40 | ? (b.fieldSelected = !b.fieldSelected, b) 41 | : (b.fieldSelected = false, b) 42 | ); 43 | // after we update the state, propogate this to the parent 44 | this.setState({ buttons }, () => this.props.getFieldChanged({ [`${fieldName}Value`]: buttons })); 45 | } 46 | 47 | render() { 48 | const { className, buttonClassName, hasError, getFieldChanged } = this.props; 49 | const { buttons } = this.state; 50 | 51 | return ( 52 |
53 | { 54 | buttons.map(({ fieldName, labelText, fieldValue, fieldSelected, className }, i) => 55 | this.getFieldChanged(field)} 64 | /> 65 | ) 66 | } 67 |
68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /both/containers/AccordionGroup/components/Accordion/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class Accordion extends React.Component { 5 | 6 | constructor() { 7 | super(); 8 | 9 | this.state = { 10 | currentStyle: { height: '0' }, 11 | }; 12 | } 13 | 14 | height = 0; 15 | 16 | static propTypes = { 17 | data : PropTypes.object.isRequired, 18 | toggleAccordion: PropTypes.func.isRequired, 19 | }; 20 | 21 | componentDidMount() { 22 | window.addEventListener('resize', this.resizeHandler) 23 | this.resizeHandler(); 24 | } 25 | 26 | componentWillReceiveProps(nextProps) { 27 | this.updateHeight(); 28 | } 29 | 30 | componentWillUnmount() { 31 | window.removeEventListener('resize', this.resizeHandler) 32 | } 33 | 34 | resizeHandler = ()=> { 35 | this.height = this.childElements.getBoundingClientRect().height; 36 | this.props.data.height = this.height; 37 | this.updateHeight(); 38 | } 39 | 40 | updateHeight = () => { 41 | const { data: { open, height } } = this.props; 42 | const currentStyle = (open) 43 | ? { height: `${height}px` } 44 | : { height: '0'}; 45 | this.setState({ currentStyle }); 46 | } 47 | 48 | toggleAccordion = () => { 49 | this.resizeHandler(); 50 | this.props.toggleAccordion(this.props.data); 51 | } 52 | 53 | render() { 54 | const { data: { id, headingMessage, open, children, subHeadingMessage } } = this.props; 55 | const { currentStyle } = this.state; 56 | return ( 57 |
58 |
this.childElements = content} 60 | className="accordion__content-measure posa l150% w100%" 61 | > 62 | {children} 63 |
64 |
68 |
69 |
70 | {headingMessage} 71 |
72 | {subHeadingMessage && 73 |
{subHeadingMessage}
74 | } 75 | 76 | + 77 | 78 | 79 |
80 |
81 |
85 | {children} 86 |
87 |
88 | ); 89 | } 90 | } -------------------------------------------------------------------------------- /server/views/layout.js: -------------------------------------------------------------------------------- 1 | 2 | const CDN_URL = process.env.CLOUDFRONT_BASE_URL; 3 | const NODE_ENV = process.env.NODE_ENV; 4 | const HASH = process.env.WEBPACK_HASH; 5 | 6 | /** 7 | * Creates a string representing the url a css file is downloaded from 8 | * @param {String} scriptName the filename of the css 9 | * @return {String} the url used for downloading the css 10 | */ 11 | const generateStyleHref = () => ( 12 | NODE_ENV === 'staged' ? `${CDN_URL}assets/staged/styles-${HASH}.css` 13 | : NODE_ENV === 'production' ? `${CDN_URL}assets/styles-${HASH}.css` 14 | : '/styles.css' 15 | ) 16 | 17 | /** 18 | * Creates a string representing the url a javaScript file is downloaded from 19 | * @param {String} scriptName the filename of the script 20 | * @return {String} the url used for downloading the script 21 | */ 22 | const generateScriptSrc = scriptName => ( 23 | NODE_ENV === 'staged' ? `${CDN_URL}assets/staged/${scriptName}-${HASH}.min.js` 24 | : NODE_ENV === 'production' ? `${CDN_URL}assets/${scriptName}-${HASH}.min.js` 25 | : `/${scriptName}.js` 26 | ) 27 | 28 | /** 29 | * This renders the page. It injects the body of the site rendered from react 30 | * 31 | * @param {object} head All the data for the head of the site 32 | * @param {string} app The rendered react application 33 | * @param {string} scriptName The name of the javascript file we are loading 34 | * @param {object} initialState The initial state of the redux store 35 | * @param {bool} hasUser True if there is a user logged in to keystone 36 | * @return {string} A string that is returned through the http(s) response to the client 37 | */ 38 | const renderLayout = (head, app, scriptName, initialState, hasUser) => ` 39 | 40 | 41 | 42 | 43 | ${head.meta.toString()} 44 | ${head.link.toString()} 45 | ${head.title.toString()} 46 | 50 | 51 | 52 |
${app}
53 | 54 | 60 | 61 | 62 | 63 | 64 | `; 65 | 66 | export default renderLayout; 67 | -------------------------------------------------------------------------------- /both/inputs/InputText/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class InputText extends React.Component { 5 | 6 | static propTypes = { 7 | className : PropTypes.string, 8 | inputClassName : PropTypes.string, 9 | fieldName : PropTypes.string.isRequired, 10 | fieldValue : PropTypes.string.isRequired, 11 | getFieldChanged : PropTypes.func.isRequired, 12 | setFieldDirty : PropTypes.func.isRequired, 13 | labelText : PropTypes.string, 14 | inputType : PropTypes.string, 15 | autocomplete : PropTypes.string, 16 | maxLength : PropTypes.number, 17 | hasError : PropTypes.bool, 18 | disabled : PropTypes.bool, 19 | selectOnFocus : PropTypes.bool, 20 | onKeyUp : PropTypes.func, 21 | valueFormatter : PropTypes.func, 22 | blurFormatter : PropTypes.func, 23 | } 24 | 25 | shouldComponentUpdate(nextProps) { 26 | return this.props !== nextProps; 27 | } 28 | 29 | render() { 30 | const { 31 | className, 32 | inputClassName, 33 | fieldName, 34 | fieldValue, 35 | getFieldChanged, 36 | setFieldDirty, 37 | labelText, 38 | inputType, 39 | autocomplete, 40 | maxLength, 41 | hasError, 42 | disabled, 43 | selectOnFocus, 44 | onKeyUp, 45 | valueFormatter, 46 | blurFormatter, 47 | } = this.props; 48 | 49 | const extraInputAttributes = { 50 | ...(maxLength ? { maxLength } : {}), 51 | ...(autocomplete ? { autoComplete : autocomplete } : {}), 52 | }; 53 | 54 | return ( 55 |
56 | {labelText && 57 | 64 | } 65 | 0 ? 'input-text__input--has-value' : ''} ${inputClassName || ''}`} 67 | name={fieldName} 68 | value={fieldValue} 69 | type={inputType || 'text'} 70 | onFocus={() => selectOnFocus && this.input.select()} 71 | ref={input => this.input = input} 72 | {...disabled ? { disabled: true } : {}} 73 | onChange={() => getFieldChanged({ 74 | [fieldName + 'Value']: valueFormatter ? valueFormatter(this.input.value, this.input) : this.input.value 75 | }) 76 | } 77 | onBlur={() => { 78 | if(blurFormatter) 79 | getFieldChanged({ 80 | [fieldName + 'Value']: blurFormatter(this.input.value) 81 | }) 82 | setFieldDirty({[fieldName + 'Dirty']: true}); 83 | }} 84 | onKeyUp={e => onKeyUp && onKeyUp(e, this.input)} 85 | { ...extraInputAttributes } 86 | /> 87 |
88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keystone4-universal-react", 3 | "version": "1.0.0", 4 | "description": "Boilerplate for Keystonejs site using React with Server Side Rendering", 5 | "private": true, 6 | "scripts": { 7 | "setup": "npm install --no-save && mkdir mongo_data && cp .env.dev .env", 8 | "setup-staged": "npm install --no-save && cp .env.staged .env", 9 | "setup-prod": "npm install --no-save && cp .env.prod .env", 10 | "clean": "rm -rf ./build-new && mkdir ./build-new", 11 | "build-client": "cross-env webpack -p && cp -a ./public ./build-new/public", 12 | "build-both": "babel -d ./build-new/both ./both -s", 13 | "build-server": "babel -d ./build-new/server ./server -s", 14 | "build-config": "cp ./.env.tmp ./build-new/.env", 15 | "build-no-install": "npm run clean && cp .env .env.tmp && npm run build-client && npm run build-both && npm run build-server && npm run build-config && rm -rf ./build && cp -rf ./build-new ./build", 16 | "build": "npm install --no-save && npm run build-no-install", 17 | "deploy": "git pull && npm run build && pm2 restart 0", 18 | "backup": "rm -rf ./build-bak && cp -rf ./build ./build-bak", 19 | "revert": "rm -rf ./build && cp -rf ./build-bak ./build", 20 | "mongod": "mongod --dbpath mongo_data", 21 | "webpack": "cross-env env UV_THREADPOOL_SIZE=100 webpack -w", 22 | "nodemon": "cross-env env UV_THREADPOOL_SIZE=100 nodemon server/index.js --watch ./server --exec babel-node", 23 | "nodemon-all": "cross-env env UV_THREADPOOL_SIZE=100 nodemon server/index.js --watch ./server --watch ./both --exec babel-node", 24 | "start-no-install": "npm run nodemon-all", 25 | "start": "npm install --no-save && npm run nodemon-all" 26 | }, 27 | "author": "Erik Christianson", 28 | "babel": { 29 | "presets": [ 30 | [ 31 | "env", 32 | { 33 | "targets": { 34 | "browsers": [ 35 | ">0.25%", 36 | "not ie 11", 37 | "not op_mini all" 38 | ] 39 | } 40 | } 41 | ], 42 | "react", 43 | "es2015", 44 | "stage-2" 45 | ], 46 | "plugins": [ 47 | "transform-decorators-legacy" 48 | ], 49 | "env": { 50 | "production": { 51 | "plugins": [ 52 | "transform-react-remove-prop-types" 53 | ] 54 | } 55 | } 56 | }, 57 | "license": "ISC", 58 | "dependencies": { 59 | "atomic-scss": "^3.0.1", 60 | "aws-sdk": "^2.307.0", 61 | "connect-mongo": "^2.0.0", 62 | "dotenv": "^6.0.0", 63 | "fuse.js": "^3.2.1", 64 | "history": "^4.7.2", 65 | "keystone": "^4.0.0", 66 | "keystone-storage-adapter-s3": "^2.0.0", 67 | "loadable-components": "^2.2.3", 68 | "mongoose": "^4.12.5", 69 | "mongoose-encryption": "^1.5.0", 70 | "nodemailer": "^4.3.1", 71 | "prop-types": "^15.6.1", 72 | "react": "^16.7.0", 73 | "react-dom": "^16.7.0", 74 | "react-helmet": "^5.2.0", 75 | "react-redux": "^5.0.7", 76 | "react-router": "^4.3.0", 77 | "react-router-config": "^1.0.0-beta.4", 78 | "react-router-dom": "^4.2.2", 79 | "react-router-redux": "^5.0.0-alpha.9", 80 | "redux": "^4.0.0", 81 | "redux-connect": "^8.0.0", 82 | "scss-functional-helpers": "^1.0.1", 83 | "underscore": "^1.8.3", 84 | "whatwg-fetch": "^2.0.4" 85 | }, 86 | "devDependencies": { 87 | "babel-cli": "^6.26.0", 88 | "babel-core": "^6.26.0", 89 | "babel-loader": "^7.1.5", 90 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 91 | "babel-plugin-transform-react-remove-prop-types": "^0.4.10", 92 | "babel-preset-env": "^1.6.1", 93 | "babel-preset-es2015": "^6.24.1", 94 | "babel-preset-react": "^6.24.1", 95 | "babel-preset-stage-2": "^6.24.1", 96 | "babel-register": "^6.24.1", 97 | "compression-webpack-plugin": "^1.1.11", 98 | "cross-env": "^5.2.0", 99 | "css-loader": "^1.0.0", 100 | "file-loader": "^2.0.0", 101 | "mini-css-extract-plugin": "^0.4.0", 102 | "node-sass": "^4.8.3", 103 | "nodemon": "^1.12.1", 104 | "sass-loader": "^7.1.0", 105 | "style-loader": "^0.23.0", 106 | "webpack": "^4.16.0", 107 | "webpack-bundle-analyzer": "^2.11.1", 108 | "webpack-cli": "^2.1.5", 109 | "webpack-s3-plugin": "^1.0.0-rc.0" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 7 | const CompressionPlugin = require("compression-webpack-plugin") 8 | const S3Plugin = require('webpack-s3-plugin'); 9 | 10 | const env = process.env.NODE_ENV || 'dev'; 11 | 12 | console.log('NODE_ENV =', env); 13 | 14 | let config = { 15 | mode: (env === 'dev') ? 'development' : 'production', 16 | entry: { 17 | main : './client/main.js', 18 | login : './client/login.js', 19 | vendor : ['react', 'react-dom', 'react-router', 'react-redux', 'react-router-dom', 'redux', 'redux-connect', 'react-router-config', 'whatwg-fetch'], 20 | }, 21 | output: { 22 | path: __dirname + '/public', 23 | publicPath: ( 24 | (env === 'staged') ? process.env.CLOUDFRONT_BASE_URL + 'assets/staged/' 25 | : (env === 'production') ? process.env.CLOUDFRONT_BASE_URL + 'assets/' 26 | : '/' 27 | ), 28 | filename: (env === 'dev') ? '[name].js' : '[name]-[hash].min.js', 29 | }, 30 | optimization: { 31 | splitChunks: { 32 | cacheGroups: { 33 | vendor: { 34 | chunks: 'initial', 35 | name: 'vendor', 36 | test: 'vendor', 37 | enforce: true 38 | }, 39 | } 40 | }, 41 | runtimeChunk: false, 42 | }, 43 | module: { 44 | rules: [ 45 | { 46 | test: /\.(jsx|js)$/, 47 | exclude: /node_modules/, 48 | loader: 'babel-loader', 49 | query: { 50 | plugins: [ 51 | 'transform-decorators-legacy', 52 | ...( 53 | env === 'staged' || env === 'production' 54 | ? ['transform-react-remove-prop-types'] 55 | : [] 56 | ) 57 | ], 58 | presets: [ 59 | ['env', { 60 | 'targets': { 61 | 'browsers': [ 62 | '>0.25%', 63 | 'not ie 11', 64 | 'not op_mini all', 65 | ] 66 | } 67 | }], 68 | 'react', 69 | 'es2015', 70 | 'stage-2', 71 | ], 72 | } 73 | }, 74 | { 75 | test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, 76 | loader: 'file-loader', 77 | }, 78 | { 79 | test: /\.scss$/, 80 | use: [ 81 | MiniCssExtractPlugin.loader, 82 | "css-loader", 83 | "sass-loader?sourceMap" 84 | ] 85 | }, 86 | ], 87 | }, 88 | plugins: [ 89 | new MiniCssExtractPlugin({ 90 | filename: (env === 'dev') ? 'styles.css' : 'styles-[hash].css', 91 | }), 92 | // new BundleAnalyzerPlugin(), 93 | ] 94 | }; 95 | 96 | if(env === 'dev') { 97 | config.devtool = 'source-map'; 98 | } 99 | 100 | if (env === 'staged' || env === 'production') { 101 | config.devtool = 'nosources-source-map'; 102 | 103 | config.plugins.push( 104 | new webpack.DefinePlugin({ 105 | 'process.env': { 106 | NODE_ENV: JSON.stringify('production') 107 | } 108 | }), 109 | new webpack.LoaderOptionsPlugin({ 110 | minimize: true, 111 | debug: false 112 | }), 113 | new webpack.EnvironmentPlugin({ 114 | NODE_ENV: 'production', 115 | DEBUG: false, 116 | }), 117 | new webpack.optimize.ModuleConcatenationPlugin(), 118 | // Don't use this if you aren't serving the files up as gzip 119 | new CompressionPlugin({ 120 | include: /.*\.(css|js)/, 121 | asset: '[file]', 122 | }), 123 | // This is a plugin to write to the .env file 124 | function () { 125 | this.plugin("done", function (stats) { 126 | const appendString = `WEBPACK_HASH=${stats.hash}`; 127 | fs.appendFile('.env.tmp', appendString, err => { 128 | if (err) throw err; 129 | console.log(`Wrote to .env.tmp: ${appendString}`); 130 | }); 131 | }); 132 | }, 133 | new S3Plugin({ 134 | include: /.*\.(css|js)/, 135 | s3Options: { 136 | accessKeyId : process.env.S3_KEY, 137 | secretAccessKey : process.env.S3_SECRET, 138 | region : process.env.S3_REGION, 139 | }, 140 | s3UploadOptions: { 141 | Bucket: process.env.S3_BUCKET, 142 | ContentEncoding(fileName) { 143 | return 'gzip'; 144 | }, 145 | }, 146 | basePath: ( 147 | (env === 'staged') ? 'assets/staged' 148 | : (env === 'production') ? 'assets' 149 | : '/' 150 | ), 151 | }) 152 | ); 153 | } 154 | module.exports = config; 155 | 156 | -------------------------------------------------------------------------------- /both/inputs/InputAddress/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import InputText from '../InputText'; 4 | import InputSelect from '../InputSelect'; 5 | import InputError from '../InputError'; 6 | import InputSelectState from '../InputSelectState'; 7 | 8 | const InputAddress = ({ 9 | className, 10 | fieldNamePrefix, 11 | hasNameField, 12 | nameValue, 13 | nameIsRequired, 14 | nameRequiredError, 15 | addressValue, 16 | addressRequiredError, 17 | address2Value, 18 | cityValue, 19 | cityRequiredError, 20 | stateValue, 21 | stateRequiredError, 22 | hasZipField, 23 | zipValue, 24 | zipRequiredError, 25 | handleFieldChanged, 26 | }) => ( 27 |
28 | {hasNameField && 29 |
30 | 38 | {nameIsRequired && 39 | Name is a required field. } 42 | hasError={nameRequiredError} 43 | /> 44 | } 45 |
46 | } 47 |
48 |
49 | 57 | Address is a required field. } 60 | hasError={addressRequiredError} 61 | /> 62 |
63 |
64 | 72 |
73 |
74 |
75 |
76 | 84 | City is a required field. } 87 | hasError={cityRequiredError} 88 | /> 89 |
90 |
91 | 100 | State is a required field } 103 | hasError={stateRequiredError} 104 | /> 105 |
106 | {hasZipField && 107 |
108 | 116 | A valid zip is required. } 119 | hasError={zipRequiredError} 120 | /> 121 |
122 | } 123 |
124 |
125 | ) 126 | 127 | InputAddress.propTypes = { 128 | className : PropTypes.string, 129 | fieldNamePrefix : PropTypes.string.isRequired, 130 | hasNameField : PropTypes.bool.isRequired, 131 | nameValue : PropTypes.string, 132 | nameIsRequired : PropTypes.bool, 133 | nameRequiredError : PropTypes.bool, 134 | addressValue : PropTypes.string.isRequired, 135 | addressRequiredError: PropTypes.bool.isRequired, 136 | address2Value : PropTypes.string.isRequired, 137 | cityValue : PropTypes.string.isRequired, 138 | cityRequiredError : PropTypes.bool.isRequired, 139 | stateValue : PropTypes.string.isRequired, 140 | stateRequiredError : PropTypes.bool.isRequired, 141 | hasZipField : PropTypes.bool.isRequired, 142 | zipValue : PropTypes.string, 143 | zipRequiredError : PropTypes.bool, 144 | handleFieldChanged : PropTypes.func.isRequired, 145 | }; 146 | 147 | export default InputAddress; 148 | -------------------------------------------------------------------------------- /both/inputs/InputTypeahead/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import fetch from 'isomorphic-fetch'; 4 | 5 | import InputText from '../../inputs/InputText'; 6 | 7 | import debounce from '../../util/debounce'; 8 | 9 | /** 10 | * This function will have its this bound to the component. This is done so we 11 | * can debounce the typeahead. 12 | * 13 | * Otherwise, this function submits a request and handles the result 14 | */ 15 | function fetchData() { 16 | const { apiPath, requestBodyKey, responseLens, typeaheadResultCallback } = this.props; 17 | const { typeaheadFieldValue } = this.state; 18 | const options = { 19 | credentials: 'include', 20 | method: 'POST', 21 | headers: { 22 | 'Accept' : 'application/json', 23 | 'Content-Type' : 'application/json' 24 | }, 25 | body: JSON.stringify({ [requestBodyKey] : typeaheadFieldValue}), 26 | }; 27 | 28 | fetch(apiPath, options) 29 | .then(r => r.json()) 30 | .then(results => { 31 | const typeaheadResults = responseLens(results); 32 | this.setState({ typeaheadResults }); 33 | if(typeaheadResultCallback) 34 | typeaheadResultCallback({ 35 | value : typeaheadFieldValue, 36 | result : typeaheadResults, 37 | }); 38 | }) 39 | .catch(err => console.error('Error', err)) 40 | } 41 | 42 | export default class InputTypeahead extends React.Component { 43 | 44 | constructor() { 45 | super() 46 | this.state = { 47 | typeaheadFieldValue : '', 48 | typeaheadResults : [], 49 | } 50 | } 51 | 52 | static propTypes = { 53 | className : PropTypes.string, 54 | inputClassName : PropTypes.string, 55 | fieldName : PropTypes.string.isRequired, 56 | fieldValue : PropTypes.string, 57 | getFieldChanged : PropTypes.func.isRequired, 58 | setFieldDirty : PropTypes.func.isRequired, 59 | labelText : PropTypes.string, 60 | inputType : PropTypes.string, 61 | autocomplete : PropTypes.string, 62 | maxLength : PropTypes.number, 63 | hasError : PropTypes.bool, 64 | selectOnFocus : PropTypes.bool, 65 | onKeyUp : PropTypes.func, 66 | valueFormatter : PropTypes.func, 67 | blurFormatter : PropTypes.func, 68 | 69 | // specific to typeahead component 70 | apiPath : PropTypes.string.isRequired, 71 | requestBodyKey : PropTypes.string.isRequired, 72 | responseLens : PropTypes.func.isRequired, 73 | typeaheadResultsRenderer : PropTypes.func.isRequired, 74 | typeaheadResultCallback : PropTypes.func, 75 | getClearHandle : PropTypes.func, 76 | debounceDelay : PropTypes.number, 77 | } 78 | 79 | // this function will be re-assigned later when the component mounts 80 | fetchData = () => {} 81 | 82 | componentDidMount() { 83 | const { debounceDelay, getClearHandle } = this.props; 84 | // debounce the fetch data function 85 | this.fetchData = debounce.call(this, fetchData, debounceDelay || 100); 86 | 87 | // if they provided a method to get a function to clear from outside this component 88 | if(getClearHandle) 89 | getClearHandle(() => this.clearTypeahead()); 90 | } 91 | 92 | /** 93 | * Track changes locally, and call the fetch method 94 | */ 95 | getFieldChanged = change => 96 | this.setState({ ...change }, () => this.fetchData()) 97 | 98 | /** 99 | * clears the input 100 | */ 101 | clearTypeahead = () => this.setState({ typeaheadFieldValue : '', typeaheadResults : [] }) 102 | 103 | /** 104 | * Pass out the dirty flag 105 | */ 106 | setFieldDirty = () => this.props.setFieldDirty({[this.props.fieldName + 'Dirty']: true}) 107 | 108 | /** 109 | * Handles clicking on a result 110 | * @param {Object} result The data associated with the thing clicked on 111 | */ 112 | selectResult = result => { 113 | const { fieldName, getFieldChanged } = this.props; 114 | this.clearTypeahead(); 115 | getFieldChanged({ [fieldName + 'Value'] : result }); 116 | } 117 | 118 | render() { 119 | const { className, typeaheadResultsRenderer, setFieldDirty, fieldValue } = this.props; 120 | const { typeaheadFieldValue, typeaheadResults } = this.state; 121 | return ( 122 |
123 |
fieldValue && this.selectResult()} 126 | > 127 | 135 |
136 | {typeaheadResults.length > 0 && 137 |
138 |
139 | { 140 | typeaheadResults.map((result, i) => ( 141 |
this.selectResult(result)} 145 | > 146 | { typeaheadResultsRenderer(result) } 147 |
148 | )) 149 | } 150 |
151 | } 152 |
153 | } 154 |
155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /both/pages/ContactPage/containers/ContactForm/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import fetch from 'isomorphic-fetch'; 4 | 5 | import InputText from '../../../../inputs/InputText'; 6 | import InputTextarea from '../../../../inputs/InputTextarea'; 7 | import InputError from '../../../../inputs/InputError'; 8 | 9 | import { 10 | getDirtyFields, 11 | formHasErrors, 12 | } from '../../../../util/form-helpers'; 13 | 14 | const initialValues = { 15 | subjectValue : '', 16 | messageValue : '', 17 | } 18 | 19 | const initialDirty = { 20 | subjectDirty : false, 21 | messageDirty : false, 22 | } 23 | 24 | const initialErrors = { 25 | subjectRequiredError : false, 26 | messageRequiredError : false, 27 | } 28 | 29 | export default class ContactForm extends React.Component { 30 | 31 | constructor() { 32 | super(); 33 | this.state = { 34 | values : { ...initialValues }, 35 | dirty : { ...initialDirty }, 36 | errors : { ...initialErrors }, 37 | formDisabled : false, 38 | formMessage : '', 39 | }; 40 | } 41 | 42 | static propTypes = { 43 | className : PropTypes.string, 44 | } 45 | 46 | handleValueChanged = change => this.setState({ values : { ...this.state.values, ...change } }, this.validateForm) 47 | 48 | handleDirtyChanged = change => this.setState({ dirty : { ...this.state.dirty, ...change } }) 49 | 50 | /** 51 | * [description] 52 | * @return {Promise} [description] 53 | */ 54 | validateForm = () => new Promise((resolve, reject) => { 55 | const { 56 | values : { 57 | subjectValue, 58 | messageValue, 59 | }, 60 | dirty : { 61 | subjectDirty, 62 | messageDirty, 63 | } 64 | } = this.state; 65 | 66 | const errors = { ...initialErrors }; 67 | 68 | if(subjectDirty) 69 | errors.subjectRequiredError = subjectValue === ''; 70 | 71 | if(messageDirty) 72 | errors.messageRequiredError = messageValue === ''; 73 | 74 | this.setState({ errors }, resolve); 75 | }) 76 | 77 | /** 78 | * Submit request to backend api 79 | * @param {Object} enquiry The enquiry we are submitting 80 | * @return {Promise} 81 | */ 82 | submitRequest = enquiry => new Promise((resolve, reject) => { 83 | const options = { 84 | credentials: 'include', 85 | method: 'POST', 86 | headers: { 87 | 'Accept' : 'application/json', 88 | 'Content-Type': 'application/json' 89 | }, 90 | body: JSON.stringify({ 91 | enquiry : { 92 | subject : enquiry.subjectValue, 93 | message : enquiry.messageValue, 94 | } 95 | }), 96 | }; 97 | 98 | fetch('/api/contact', options) 99 | .then(r => r.json()) 100 | .then(data => { 101 | if(data.error) 102 | throw(data); 103 | else { 104 | this.setState({ formMessage : 'Thank you for your enquiry!' }); 105 | resolve(data); 106 | } 107 | }) 108 | .catch(data => { 109 | this.setState({ formDisabled : false }); 110 | reject(data) 111 | }) 112 | }) 113 | 114 | /** 115 | * Handles the submision of the form 116 | */ 117 | handleSubmit = () => this.setState({ 118 | dirty : { 119 | ...getDirtyFields(initialDirty), 120 | }, 121 | formDisabled : true, 122 | }, () => { 123 | this.validateForm() 124 | .then(() => { 125 | if(formHasErrors(initialErrors, this.state.errors)) 126 | this.setState({ formDisabled : false }); 127 | else 128 | this.submitRequest(this.state.values) 129 | // .then(data => this.props.messageSend()) 130 | // .catch(); 131 | }) 132 | }) 133 | 134 | render() { 135 | const { 136 | values : { 137 | subjectValue, 138 | messageValue, 139 | }, 140 | errors : { 141 | subjectRequiredError, 142 | messageRequiredError, 143 | }, 144 | formDisabled, 145 | formMessage, 146 | } = this.state; 147 | const { className } = this.props; 148 | return ( 149 |
150 |
e.preventDefault()} 153 | > 154 |
155 | 163 | You must provide a subject. } 166 | /> 167 |
168 |
169 | 177 | You must provide a message. } 180 | /> 181 |
182 |
183 | 190 |
191 |
192 |
{ formMessage }
193 |
194 | ); 195 | } 196 | } -------------------------------------------------------------------------------- /WIRING-IN-PAGES.md: -------------------------------------------------------------------------------- 1 | # How to Wire Pages Up to the Back-End 2 | 3 | This should provide a step by step guide for how to create a new page for a site and then provide data to it from a keystone model. 4 | 5 | ## In `both` folder 6 | 7 | ### Adding a Page 8 | 9 | Pages should be added as react components in the `both/pages/` folder. 10 | 11 | A directory should be created for the page (`both/pages/Page`). 12 | 13 | **Pro tip**: You can often copy a page folder that already exists and start renaming things. 14 | 15 | ### Files Required in a page folder 16 | 17 | * `index.js` Load component, wire in to react-redux and perform code-splitting 18 | * `component.js` the component 19 | 20 | ***Note***: This tutorial assumes that you will be code-splitting any new page that you add. 21 | 22 | ### Sample `index.js` 23 | 24 | ```javascript 25 | import React, { Component } from 'react' 26 | import { asyncConnect } from 'redux-connect' 27 | import loadable from 'loadable-components' 28 | 29 | import { 30 | pageDataLoadSuccessAction, 31 | pageDataLoadFailureAction, 32 | } from '../../global-actions'; 33 | 34 | import { apiRequest } from '../../util/api-request'; 35 | 36 | const Page = loadable(() => 37 | import(/* webpackChunkName: "-page" */'./component') 38 | ) 39 | 40 | const mapStateToProps = state => ({ 41 | pageData : state.appReducer.pageData, 42 | }) 43 | 44 | @asyncConnect([{ 45 | promise: ({ params, helpers, store: { dispatch }, data }) => 46 | apiRequest('page', {}, data) 47 | .then(({ data: { pageData } }) => dispatch(pageDataLoadSuccessAction(pageData))) 48 | }], mapStateToProps) 49 | export default class Page extends React.Component { 50 | render() { 51 | return 52 | } 53 | } 54 | 55 | ``` 56 | 57 | ### Sample `component.js` 58 | 59 | ```javascript 60 | import React from 'react'; 61 | import PropTypes from 'prop-types'; 62 | import Helmet from 'react-helmet'; 63 | import { asyncConnect } from 'redux-connect'; 64 | 65 | import { 66 | pageDataLoadSuccessAction, 67 | pageDataLoadFailureAction, 68 | } from '../../global-actions'; 69 | 70 | import { apiRequest } from '../../util/api-request'; 71 | 72 | const mapStateToProps = state => ({}); 73 | 74 | @asyncConnect([{ 75 | promise: ({ params, helpers, store: { dispatch }, data }) => 76 | apiRequest('page/', {}, data) 77 | .then(({ data: { pageData } }) => dispatch(pageDataLoadSuccessAction(pageData))) 78 | }], mapStateToProps) 79 | export default class Page extends React.Component { 80 | 81 | static propTypes = { 82 | pageData: PropTypes.object, 83 | }; 84 | 85 | render() { 86 | const { pageData } = this.props; 87 | 88 | return ( 89 |
90 | 93 | { PageData } 94 |
95 | ); 96 | } 97 | } 98 | ``` 99 | 100 | ### Additional Files used for new pages 101 | 102 | * If the page performs actions that should be preserved between pages, then you should use redux 103 | * `constants.js` redux action types 104 | * `actions.js` the action creators 105 | * `reducer.js` the redux reducer 106 | * If you want static content, and don't want to polute the `component.js` file with it, then import it from an external file 107 | * `content.js` 108 | 109 | #### Update Combined Reducers (`both/reducers.js`) (Only `reducer.js` was Created) 110 | 111 | ```javascript 112 | import { combineReducers } from 'redux'; 113 | import appReducer from './app-reducer'; 114 | import { reducer as reduxAsyncConnect } from 'redux-connect'; 115 | import homePageReducer from '../pages/HomePage/reducer'; 116 | import PageReducer from '../pages/Page/reducer'; 117 | 118 | export default combineReducers({ 119 | appReducer, 120 | homePageReducer, 121 | PageReducer, 122 | reduxAsyncConnect,// must be last 123 | }); 124 | ``` 125 | 126 | ### Update Client-Side Routing (`both/routes.js`) 127 | 128 | You need to add new pages to the client side routing. This is found in `both/routes.js`. Follow the example of other pages there. 129 | 130 | ```javascript 131 | 132 | const routes = [{ 133 | component: App, 134 | routes: [ 135 | { 136 | path : '/', 137 | exact : true, 138 | component: HomePage, 139 | }, 140 | { 141 | path : '/faqs', 142 | component: FaqPage, 143 | }, 144 | { 145 | path : '/contact', 146 | component: ContactPage, 147 | }, 148 | // ** You'll add something like this ** 149 | { 150 | path : '/', 151 | component: Page, 152 | }, 153 | { 154 | path : '*', 155 | component: NotFoundPage, 156 | }, 157 | ], 158 | }]; 159 | 160 | ``` 161 | 162 | ## In `server` folder 163 | 164 | On the server you'll need to create a few files and update some other files. 165 | 166 | ### Create a Model for your Page 167 | 168 | Create a file `server/models/pages/Page.js`. 169 | 170 | This file should begin by including: 171 | 172 | ```javascript 173 | import keystone from 'keystone'; 174 | const Types = keystone.Field.Types; 175 | 176 | const Page = new keystone.List('Page', { 177 | map: { name: 'title' }, 178 | autokey: { path: 'slug', from: 'title', unique: true }, 179 | nocreate: !(process.env.NODE_ENV === 'dev' || process.env.CAN_CREATE_PAGES === 'true'), 180 | nodelete: true, 181 | }); 182 | 183 | Page.add({ 184 | title: { type: String, required: true }, 185 | // put whatever other fields you need. 186 | // look at other models for examples 187 | }); 188 | 189 | Page.defaultColumns = 'title'; 190 | Page.register(); 191 | 192 | ``` 193 | 194 | ### Update Database Seed (`server/updates/0.0.1-admin.js`) (Optional) 195 | 196 | This file seeds the database on the first run of the site. Having all necessary information here and up to date helps deployments to go much smoother. 197 | 198 | ```javascript 199 | exports.create = { 200 | User: [ 201 | { 'name.first': 'Admin', 'name.last': 'User', 'email': 'user@keystonejs.com', 'password': 'admin', 'isAdmin': true }, 202 | ], 203 | SiteConfiguration: [ 204 | { title : 'Global Site Configuration' }, 205 | ], 206 | HomePage: [ 207 | { title: 'Home Page' }, 208 | ], 209 | FaqPage: [ 210 | { title: 'Faq Page' }, 211 | ], 212 | ContactPage: [ 213 | { title: 'Contact Page' }, 214 | ], 215 | // ** You'll add something like this ** 216 | Page: [ 217 | { title: ' Page' }, 218 | ], 219 | // end new page entry 220 | }; 221 | 222 | ``` 223 | 224 | ### Update CMS Model Presentation (`server/index.js`) (Optional) 225 | 226 | In `server/index.js` you'll see a section that looks something like this: 227 | 228 | ```javascript 229 | keystone.set('nav', { 230 | 'page data': [ 231 | 'home-pages', 232 | // ** You'll add something like this ** 233 | '-pages' 234 | ], 235 | }); 236 | ``` 237 | 238 | Under the page data section, add your page following the other examples. (**Note:** your page must be pluralized in this list.) 239 | 240 | ### Update Page Data Loading Routes (`server/data/index.js`) for Server-Side Rendering 241 | 242 | In `server/data/index.js` you'll see a section that looks something like this: 243 | 244 | ```javascript 245 | switch(pagePath) { 246 | // ... 247 | case 'home-Page': 248 | promises.push(getPageData(data, 'HomePage')); 249 | break; 250 | // ** You'll add something like this ** 251 | case '': 252 | promises.push(getPageData(data, 'Page')); 253 | break; 254 | //... 255 | } 256 | ``` 257 | 258 | ## Data Entry: Manually Create the Page Instance in Keystone 259 | 260 | * Log in to keystone 261 | * Go to the section of the model you just created 262 | * Create a page, and give it a title (Ideally usually whatever the page is named). 263 | * Save 264 | -------------------------------------------------------------------------------- /scss/config/_atomic-config.scss: -------------------------------------------------------------------------------- 1 | $breakpoint-order: ( 2 | $bp-lgx-mx, 3 | $bp-lg-mx , 4 | $bp-md-mx , 5 | $bp-sm-mx , 6 | $bp-smx-mx, 7 | $bp-lgx-mn, 8 | $bp-lg-mn , 9 | $bp-md-mn , 10 | $bp-sm-mn , 11 | $bp-smx-mn, 12 | ); 13 | 14 | // atomic generation configuration 15 | $atomic-config:( 16 | //(attribute, className, postfix , modifier, style variants) 17 | ('background-color', 'bgc-', '', '', $backgroundColor), 18 | ('border', 'bd', '', '', $border), 19 | ('border-top', 'bt', '', '', $border), 20 | ('border-bottom', 'bb', '', '', $border), 21 | ('border-left', 'bl', '', '', $border), 22 | ('border-right', 'br', '', '', $border), 23 | // top bottom left right 24 | ('top', 't', '', '', $ts), 25 | ('top', 't', 'vw', '', $ts-vw), 26 | ('top', 't', '\\%', '', $ts-perc), 27 | ('bottom', 'b', '', '', $bs), 28 | ('bottom', 'b', 'vw', '', $bs-vw), 29 | ('bottom', 'b', '\\%', '', $bs-perc), 30 | ('left', 'l', '', '', $ls), 31 | ('left', 'l', 'vw', '', $ls-vw), 32 | ('left', 'l', '\\%', '', $ls-perc), 33 | ('right', 'r', '', '', $rs), 34 | ('right', 'r', 'vw', '', $rs-vw), 35 | ('right', 'r', '\\%', '', $rs-perc), 36 | // end top bottom left right 37 | ('box-shadow', 'bxsh', '', '', $boxShadow), 38 | ('cursor', 'cur', '', '', $cursor), 39 | ('clear', 'cl', '', '', $clear), 40 | ('color', 'c-', '', '', $color), 41 | ('color', 'c-', '\\:h', ':hover', $color), 42 | ('display', 'd', '', '', $display), 43 | // start flexbox 44 | ('flex', 'flx', '', '', $flex), 45 | ('flex-grow', 'fxg', '', '', $flexGrow), 46 | ('flex-shring', 'fxsh', '', '', $flexShrink), 47 | ('flex-basis', 'fxb', '', '', $flexBasis), 48 | ('flex-direction', 'fxdr', '', '', $flexDirection), 49 | ('flex-wrap', 'fxw', '', '', $flexWrap), 50 | ('align-items', 'ai', '', '', $alignItems), 51 | ('align-self', 'as', '', '', $alignSelf), 52 | ('justify-content', 'jc', '', '', $justifyContent), 53 | ('order', 'ord', '', '', $order), 54 | // end flex-box 55 | ('float', 'fl', '', '', $float), 56 | // font 57 | ('font-family', 'ff', '', '', $fontFamily), 58 | ('font-size', 'fz', '', '', $fontSize), 59 | ('font-size', 'fz', 'vw', '', $fontSize-vw), 60 | ('font-style', 'fs', '', '', $fontStyle), 61 | ('font-Weight', 'fw', '', '', $fontWeight), 62 | ('letter-spacing', 'lts', '', '', $letterSpacing), 63 | // end font 64 | // grid 65 | // ??? 66 | // end grid 67 | // height 68 | ('height', 'h', '', '', $height), 69 | ('height', 'h', 'vw', '', $height-vw), 70 | ('height', 'h', '\\%', '', $height-perc), 71 | ('max-height', 'mah', '', '', $height), 72 | ('max-height', 'mah', 'vw', '', $height-vw), 73 | ('max-height', 'mah', '\\%', '', $height-perc), 74 | ('min-height', 'mih', '', '', $height), 75 | ('min-height', 'mih', 'vw', '', $height-vw), 76 | ('min-height', 'mih', '\\%', '', $height-perc), 77 | // end height 78 | ('list-style', 'ls', '', '', $listStyle), 79 | // margin 80 | ('margin', 'm', '', '', $margin), 81 | ('margin', 'm', 'vw', '', $margin-vw), 82 | ('margin', 'm', '\\%', '', $margin-perc), 83 | ('margin-top', 'mt', '', '', $margin), 84 | ('margin-top', 'mt', 'vw', '', $margin-vw), 85 | ('margin-top', 'mt', '\\%', '', $margin-perc), 86 | ('margin-bottom', 'mb', '', '', $margin), 87 | ('margin-bottom', 'mb', 'vw', '', $margin-vw), 88 | ('margin-bottom', 'mb', '\\%', '', $margin-perc), 89 | ('margin-left', 'ml', '', '', $margin), 90 | ('margin-left', 'ml', 'vw', '', $margin-vw), 91 | ('margin-left', 'ml', '\\%', '', $margin-perc), 92 | ('margin-right', 'mr', '', '', $margin), 93 | ('margin-right', 'mr', 'vw', '', $margin-vw), 94 | ('margin-right', 'mr', '\\%', '', $margin-perc), 95 | // end margin 96 | ('opacity', 'op', '', '', $opacity), 97 | ('overflow', 'o', '', '', $overflow), 98 | ('overflow-x', 'ox', '', '', $overflow), 99 | ('overflow-y', 'oy', '', '', $overflow), 100 | // padding 101 | ('padding', 'p', '', '', $padding), 102 | ('padding', 'p', 'vw', '', $padding-vw), 103 | ('padding', 'p', '\\%', '', $padding-perc), 104 | ('padding-top', 'pt', '', '', $padding), 105 | ('padding-top', 'pt', 'vw', '', $padding-vw), 106 | ('padding-top', 'pt', '\\%', '', $padding-perc), 107 | ('padding-bottom', 'pb', '', '', $padding), 108 | ('padding-bottom', 'pb', 'vw', '', $padding-vw), 109 | ('padding-bottom', 'pb', '\\%', '', $padding-perc), 110 | ('padding-left', 'pl', '', '', $padding), 111 | ('padding-left', 'pl', 'vw', '', $padding-vw), 112 | ('padding-left', 'pl', '\\%', '', $padding-perc), 113 | ('padding-right', 'pr', '', '', $padding), 114 | ('padding-right', 'pr', 'vw', '', $padding-vw), 115 | ('padding-right', 'pr', '\\%', '', $padding-perc), 116 | // end padding 117 | ('position', 'pos', '', '', $position), 118 | ('stroke', 'stroke-', '', '', $stroke), 119 | ('fill', 'fill-', '', '', $fill), 120 | // text 121 | ('text-align', 'ta', '', '', $textAlign), 122 | ('text-decoration', 'td', '', '', $textDecoration), 123 | ('text-shadow', 'txsh', '', '', $textShadow), 124 | ('text-transform', 'tt', '', '', $textTransform), 125 | ('line-height', 'lh', '', '', $lineHeight), 126 | // end text 127 | ('transform', 'trf-', '', '', $transforms), 128 | ('transition', 'trs-', '', '', $transitions), 129 | // width 130 | ('width', 'w', '', '', $width), 131 | ('width', 'w', 'vw', '', $width-vw), 132 | ('width', 'w', '\\%', '', $width-perc), 133 | ('max-width', 'maw', '', '', $width), 134 | ('max-width', 'maw', 'vw', '', $width-vw), 135 | ('max-width', 'maw', '\\%', '', $width-perc), 136 | ('min-width', 'miw', '', '', $width), 137 | ('min-width', 'miw', 'vw', '', $width-vw), 138 | ('min-width', 'miw', '\\%', '', $width-perc), 139 | // end width 140 | ('vertical-align', 'va', '', '', $verticalAlign), 141 | ('word-break', 'wob', '', '', $wordBreak), 142 | ('word-wrap', 'wow', '', '', $wordWrap), 143 | ('z-index', 'z', '', '', $zIndex), 144 | ); 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keystone4 Universal React 2 | 3 | ## Development Setup 4 | 5 | **Note** 6 | 7 | There is a `.env` file required that represents some configuration information. 8 | 9 | * If not already installed, install `git`, `mongodb` and `node` 10 | * run the following commands in a terminal: 11 | * `git clone` this repo 12 | * `cd keyston4-universal-react` 13 | * `git checkout dev` 14 | * `npm run setup` 15 | 16 | ### Running the Project 17 | 18 | Once project is setup run the following three commands in the root of the project in separate terminals: 19 | 20 | * `npm run mongod` to start the database 21 | * `npm run webpack` to start a watcher that will build the javascript 22 | * `npm start` to start the server (which will auto-restart on file change) 23 | 24 | I recommend using [iTerm2](https://www.iterm2.com/) on mac, or [cmmdr](http://cmder.net/) in windows so you can have multiple terminal panes visible at once. 25 | 26 | ## Build 27 | 28 | To build the project run `npm run build` at the root of the project. 29 | 30 | This will: 31 | * delete and recreate the `./build-new` folder 32 | * build the client code and copies `./public` folder into `./build-new` folder (deletes `scss` folder from within `./build-new/public` folder) 33 | * builds the `./both` folder to `./build-new/both` 34 | * builds the `./server` folder to `./build-new/server` 35 | * copies the current environment file (`.env`) to `./build-new/.env`. 36 | * removes current `./build` folder and then renames `./build-new` to `./build` 37 | 38 | ## Hosting on Amazon AWS 39 | 40 | ### Creating an AWS instance on EC2 41 | 42 | * Create an EC2 instance with Ubuntu 43 | * [Install mongo](https://docs.mongodb.com/v3.4/tutorial/install-mongodb-on-ubuntu/) 44 | * You can check Ubuntu version with `lsb_release -a` 45 | * `sudo apt-get install build-essential g++` 46 | * [Install Node](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions) 47 | * `sudo npm install -g pm2` 48 | * `cd ~` (to make sure you're in the home directory) 49 | * `git clone` this repository 50 | * `cd keystone4-universal-react` 51 | * If on staged: `git checkout staged` 52 | * `npm run setup-prod` (or `npm run setup-staged`) 53 | * `sudo npm run build` 54 | * `cd build` 55 | * `sudo pm2 start server/index.js` 56 | * optionally run `sudo pm2 logs 0` to see if everything is alright with the server 57 | 58 | #### `PM2` notes 59 | 60 | `PM2` is used to run the site, and to restart it if there is an error. 61 | 62 | In `build` directory (created by build step): 63 | * `sudo pm2 kill && sudo pm2 start server/index.js` (**`sudo` is necessary to run on port 80**) 64 | 65 | To stop running: 66 | * `sudo pm2 kill` to stop all server instances 67 | 68 | To restart: 69 | * `sudo pm2 restart 0` 70 | 71 | To stop running: 72 | * Use `pm2 stop 0` to stop the correct process 73 | * Use `pm2 delete 0` to delete the correct process from pm2 process list 74 | 75 | #### Database Logs 76 | 77 | The mongo database data is stored in `/var/lib/mongo/` 78 | 79 | The mongo logs are stored in `/var/logs/mongodb` 80 | 81 | ### Connecting to AWS EC2 over `ssh` 82 | 83 | In order to connect over ssh, make sure that security group settings for your EC2 instance will allow your inbound connection over ssh. 84 | 85 | To add an ip you must do the following: 86 | 87 | * Log into the EC2 console on AWS for the instance you want to change 88 | * Go to the Security Groups panel 89 | * Edit the inbound rules (type = ssh, and follow the examples of others) 90 | 91 | After that, go to the base directory of the site after cloning it and run the following command: 92 | 93 | `ssh -i ".pem" ` 94 | (If you get an error try chmod 400 .pem) 95 | 96 | * staged 97 | * `ssh -i "" ubuntu@1.1.1.1` 98 | * prod 99 | * `ssh -i "" ubuntu@1.1.1.1` 100 | 101 | ### Deploying 102 | 103 | * `ssh` into server 104 | * `cd ./keystone4-universal-react` 105 | * `sudo npm run deploy` 106 | 107 | ## Project Conventions 108 | 109 | ### JavaScript 110 | 111 | * Use ES6+ where possible. 112 | * Keep things close to data 113 | 114 | ### Files and Folders 115 | 116 | There are a number of different kinds of components broken into different folders for organization purposes. 117 | 118 | * blocks - these components have children that are passed in, and generally provide some display effect reused throughout the site. 119 | * components - these are components that take props and render - they don't have reducers 120 | * containers - these components are like normal components but smart, i.e., they may have reducers, or maintain the state of a ground of other components 121 | * inputs - smart components used for forms 122 | * global - these are like containers, but particularly are components that load everywhere on the site 123 | * pages - these are like containers, but are distinguished semantically because they represent pages (they are routed to) 124 | * svg - these are svgs. Props are passed in to control their display characteristics. 125 | 126 | If there are any components that are only used within one specific component, you can create the folder for the components inside of the other component folder (e.g., /both/global/Header contains a Navigation folder). If there are a few components that are only used within a single component, then create directories for `component`, `container`, &c. 127 | 128 | If pages are getting long, and can be broken into logical parts to keep them easier to think about, you can create a `partials` folder within the directory of that page (i.e., `/both/pages/HomePage/partials/SomePartial/index.js`). 129 | 130 | #### React 131 | 132 | This project uses the following `react` and react-oriented libraries: 133 | * `react-dom` 134 | * `react-router` 135 | * `redux` 136 | * `react-redux` 137 | * `redux-connect` 138 | 139 | There are several kinds of react components: 140 | 141 | * global - components loaded at all times 142 | * *e.g.,* header, footer 143 | * pages - represent a page 144 | * container - wrap components and provide them with data 145 | * blocks - structural elements. Used to wrap elements in order to fit them on a page in a regular way 146 | * *e.g.,* floating content, spacer content 147 | * component - simple elements that render information and fire events (passed in by containers or parents) 148 | * inputs - inputs for use in forms. These are all fairly smart components. 149 | 150 | global, page and container components can have the following files for persisting state: 151 | 152 | * `index.js` - a thin wrapper that code splits the page component being loaded 153 | * `component.js` - the component itself 154 | * If a reducer is required for the page: 155 | * `actions.js` - action creator functions 156 | * `constants.js` - constants that represent action names 157 | * `reducer.js` - the reducer for this component 158 | 159 | ### CSS (SASS) 160 | 161 | This project uses [atomic-scss](https://github.com/internetErik/atomic-scss) as much as possible. For everything that can't be done reasonably with atomic scss (which isn't a lot) we losely follow [BEM](http://getbem.com/introduction/). Tend towards giving components BEM style class names, even if you don't actually use them in any scss file. 162 | 163 | * Try to do as much styling as you can with atomic styles 164 | * If you can't, then create a scss file 165 | * The folder structure of the scss files should match the directory structure for react components 166 | * `both/global` => `public/scss/global` 167 | * `both/pages` => `public/scss/pages` 168 | * `both/containers` => `public/scss/containers` 169 | * `both/component` => `public/scss/component` 170 | * `both/blocks` => `public/scss/blocks` 171 | * `both/inputs` => `public/scss/inputs` 172 | * `both/svg` => `public/scss/svg` 173 | * The CSS class for a component should be the component name converted from camel case to hyphen case 174 | *`HomePage` => `home-page` 175 | * The scss file should be named the same as the CSS class used for the components (e.g., `home-page`) 176 | 177 | ### Server Side Rendering (SSR) 178 | 179 | This project renders the front-end on the server on the first request in a manner that allows `react` to take over on the front-end. 180 | 181 | Some components need to load data from mongo (via `keystonejs`'s mongoose interface). In order to do this, there is a folder called `server/data` that contains a file `index.js` as well as other files. 182 | 183 | #### Regarding `server/data/index.js` 184 | 185 | This file exports a function (`populateData`) that takes a url and uses it to determine what data gathering functions must be called. These data gathering functions live in the other files that are in the `server/data` folder. 186 | 187 | #### Methods For Getting Data 188 | 189 | All functions used by `populateData` to lookup data must return a `Promise`. They must also take an object as a first argument. Any data fetched from mongo will be assigned to this object before the `Promise` resolves. 190 | 191 | #### Getting More Data (On the `populateData` Function) 192 | 193 | If you need to get more data out of mongo you'll need to add code to `populateData`. 194 | 195 | ## ToDos 196 | 197 | ### Babel 7 198 | 199 | This is waiting on keystone support (I think) 200 | 201 | ### Is `react-router-config` still necessary? 202 | 203 | ### Webpack Enhancements 204 | 205 | * Read sass variable files in with [sass-variable-loader](https://www.npmjs.com/package/sass-variable-loader). This would help us to have one place where we kept breakpoint values. 206 | -------------------------------------------------------------------------------- /scss/config/_atomic-variables.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Start style config variables 3 | // 4 | 5 | // background-color bgc 6 | $backgroundColor : ( 7 | (vals: ( 8 | 'white': $color-white, 9 | 'lightgray': $color-lightgray, 10 | 'gray': $color-gray, 11 | 'black': $color-black, 12 | 'black-\\.5a': rgba($color-black, .5), 13 | )), 14 | ); 15 | 16 | // border bd, bt, bb, bl, br 17 | $border: ( 18 | (vals:( 19 | '1-s-white' : '1px solid ' + $color-white, 20 | '1-s-lightgray': '1px solid ' + $color-lightgray, 21 | '1-s-gray' : '1px solid ' + $color-gray, 22 | '1-s-black' : '1px solid ' + $color-black, 23 | )), 24 | ); 25 | 26 | // border-radius bdrs 27 | $borderRadius: ( 28 | (vals: ('0': 0, '1': 1px,'2': 2px,'3': 3px,'4': 4px, '5': 5px, '6': 6px, '7': 7px, '8': 8px, '9': 9px, '10': 10px,)), 29 | ); 30 | 31 | // top t, bottom b, left l, right r 32 | $ts: ( 33 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,)), 34 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-lg-mx,), 35 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-md-mx,), 36 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-sm-mx,), 37 | ); 38 | 39 | $ts-vw: ( 40 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,)), 41 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-lg-mx,), 42 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-md-mx,), 43 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-sm-mx,), 44 | ); 45 | 46 | $ts-perc: ( 47 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,)), 48 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-lg-mx,), 49 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-md-mx,), 50 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-sm-mx,), 51 | ); 52 | 53 | $bs: ( 54 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,)), 55 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-lg-mx,), 56 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-md-mx,), 57 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-sm-mx,), 58 | ); 59 | 60 | $bs-vw: ( 61 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,)), 62 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-lg-mx,), 63 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-md-mx,), 64 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-sm-mx,), 65 | ); 66 | 67 | $bs-perc: ( 68 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,)), 69 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-lg-mx,), 70 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-md-mx,), 71 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-sm-mx,), 72 | ); 73 | 74 | $ls: ( 75 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,)), 76 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-lg-mx,), 77 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-md-mx,), 78 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-sm-mx,), 79 | ); 80 | 81 | $ls-vw: ( 82 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,)), 83 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-lg-mx,), 84 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-md-mx,), 85 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-sm-mx,), 86 | ); 87 | 88 | $ls-perc: ( 89 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,)), 90 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-lg-mx,), 91 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-md-mx,), 92 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-sm-mx,), 93 | ); 94 | 95 | $rs: ( 96 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,)), 97 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-lg-mx,), 98 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-md-mx,), 99 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-sm-mx,), 100 | ); 101 | 102 | $rs-vw: ( 103 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,)), 104 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-lg-mx,), 105 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-md-mx,), 106 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-sm-mx,), 107 | ); 108 | 109 | $rs-perc: ( 110 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,)), 111 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-lg-mx,), 112 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-md-mx,), 113 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-sm-mx,), 114 | ); 115 | 116 | // clear cl 117 | $clear: ( 118 | (vals: ('b': 'both', 'l': 'left', 'r': 'right')), 119 | ); 120 | 121 | // color c 122 | $color : ( 123 | (vals: ( 124 | 'black' : $color-black, 125 | 'white' : $color-white, 126 | 'lightgray': $color-lightgray, 127 | 'gray' : $color-gray, 128 | )), 129 | ); 130 | 131 | //box-shadow txsh 132 | $boxShadow: ( 133 | (vals: ( 134 | '1-1-1-1-white' : 1px 1px 1px 1px $color-white, 135 | '1-1-1-1-lightgray': 1px 1px 1px 1px $color-lightgray, 136 | '1-1-1-1-gray' : 1px 1px 1px 1px $color-gray, 137 | '1-1-1-1-black' : 1px 1px 1px 1px $color-black, 138 | ),), 139 | ); 140 | 141 | // cursor cur 142 | $cursor: ( 143 | (vals: ('p': pointer, 'c': crosshair, 'm': move, 'ha': hand, 'he': help)), 144 | ); 145 | 146 | // display d 147 | $display: ( 148 | (vals: ('n': none, '-i': inline, 'b': block, 'ib': 'inline-block', 'f': flex, 'if': inline-flex, 'g': grid)), 149 | (vals: ('n': none, '-i': inline, 'b': block, 'ib': 'inline-block', 'f': flex, 'if': inline-flex, 'g': grid), bp: $bp-lg-mx), 150 | (vals: ('n': none, '-i': inline, 'b': block, 'ib': 'inline-block', 'f': flex, 'if': inline-flex, 'g': grid), bp: $bp-md-mx), 151 | (vals: ('n': none, '-i': inline, 'b': block, 'ib': 'inline-block', 'f': flex, 'if': inline-flex, 'g': grid), bp: $bp-sm-mx), 152 | ); 153 | 154 | //******************** 155 | // flex-box 156 | //******************** 157 | 158 | // flex flx 159 | $flex: ( 160 | (vals: ('1': 1,'2': 2,'3': 3,'4': 4, '5': 5,)), 161 | ); 162 | 163 | // flex-grow fxg 164 | $flexGrow: ( 165 | (vals: ('1': 1,'2': 2,'3': 3,'4': 4, '5': 5,)), 166 | ); 167 | 168 | // flex-Shring fxsh 169 | $flexShrink: ( 170 | (vals: ('1': 1,'2': 2,'3': 3,'4': 4, '5': 5,)), 171 | ); 172 | 173 | // flex-basis fxb 174 | $flexBasis: ( 175 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,),), 176 | ); 177 | 178 | // flex-direction fxdr 179 | $flexDirection: ( 180 | (vals: ('r': row, 'c': column, 'rr': row-reverse, 'cr': column-reverse),), 181 | (vals: ('r': row, 'c': column, 'rr': row-reverse, 'cr': column-reverse), bp: $bp-lg-mx), 182 | (vals: ('r': row, 'c': column, 'rr': row-reverse, 'cr': column-reverse), bp: $bp-md-mx), 183 | (vals: ('r': row, 'c': column, 'rr': row-reverse, 'cr': column-reverse), bp: $bp-sm-mx), 184 | ); 185 | 186 | // flex-wrap fxw 187 | $flexWrap: ( 188 | (vals: ('n': nowrap, 'w': wrap, 'r': wrap-reverse),), 189 | ); 190 | 191 | // align-items ai 192 | $alignItems: ( 193 | (vals: ('fs': flex-start, 'fe': flex-end, 'b': baseline, 'c': center, 's': stretch),), 194 | ); 195 | 196 | // align-self as 197 | $alignSelf: ( 198 | (vals: ('b': baseline, 'c': center, 'a': auto),), 199 | ); 200 | 201 | // justify-content jc 202 | $justifyContent: ( 203 | (vals: ('fs': flex-start, 'fe': flex-end, 'c': center, 'sb': space-between, 'sa': space-around),), 204 | (vals: ('fs': flex-start, 'fe': flex-end, 'c': center, 'sb': space-between, 'sa': space-around), bp: $bp-lg-mx), 205 | (vals: ('fs': flex-start, 'fe': flex-end, 'c': center, 'sb': space-between, 'sa': space-around), bp: $bp-md-mx), 206 | (vals: ('fs': flex-start, 'fe': flex-end, 'c': center, 'sb': space-between, 'sa': space-around), bp: $bp-sm-mx), 207 | ); 208 | 209 | // order ord 210 | $order: ( 211 | (vals: ('1': 1,'2': 2,'3': 3,'4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, '99': 99,)), 212 | ); 213 | //******************** 214 | // end flexbox 215 | //******************** 216 | 217 | // float 218 | $float: ( 219 | (vals: ('l': left, 'r': right, 'n': none),), 220 | (vals: ('l': left, 'r': right, 'n': none), bp: $bp-lg-mx), 221 | (vals: ('l': left, 'r': right, 'n': none), bp: $bp-md-mx), 222 | (vals: ('l': left, 'r': right, 'n': none), bp: $bp-sm-mx), 223 | ); 224 | 225 | //******************** 226 | // font 227 | //******************** 228 | 229 | // font-family ff 230 | $fontFamily: ( 231 | (vals: ('s': serif, 'ss': sans-serif),), 232 | ); 233 | 234 | // font-size fz 235 | $fzs: ('0': 0, '10': 10px, '12': 12px, '13': 13px, '14': 14px, '15': 15px, '16': 16px, '17': 17px, '18': 18px, '19': 19px, '20': 20px, '21': 21px, '22': 22px, '23': 23px, '24': 24px, '25': 25px, '26': 26px, '27': 27px, '28': 28px, '32': 32px, '34': 34px, '36': 36px, '42': 42px, '48': 48px,); 236 | $fzs-vw: ('0': 0vw, '1': 1vw, '1\\.3': 1.3vw, '2': 2vw, '2\\.5': 2.5vw, '3': 3vw, '4': 4vw, '5': 5vw, '10': 10vw, '15': 15vw,); 237 | $fontSize: ( 238 | (vals: $fzs), 239 | (vals: $fzs, bp: $bp-lg-mx, ), 240 | (vals: $fzs, bp: $bp-md-mx, ), 241 | (vals: $fzs, bp: $bp-sm-mx, ), 242 | ); 243 | 244 | $fontSize-vw: ( 245 | (vals: $fzs-vw), 246 | (vals: $fzs-vw, bp: $bp-lg-mx, ), 247 | (vals: $fzs-vw, bp: $bp-md-mx, ), 248 | (vals: $fzs-vw, bp: $bp-sm-mx, ), 249 | ); 250 | 251 | // font-style fs 252 | $fontStyle: ( 253 | (vals: ('i': italic, 'o': oblique),), 254 | ); 255 | 256 | // font-weight fw 257 | $fontWeight: ( 258 | (vals: ('100': 100, '200': 200, '300': 300, '400': 400, '500': 500, '600': 600, '700': 700, '800': 800, '900': 900,),), 259 | ); 260 | 261 | // letter-spacing lts 262 | $letterSpacing: ( 263 | (vals: ('1': 1px, '2': 2px, '3': 3px, '4': 4px, '5': 5px,)), 264 | ); 265 | //******************** 266 | // end font 267 | //******************** 268 | 269 | // height h 270 | $height: ( 271 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,)), 272 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-lg-mx,), 273 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-md-mx,), 274 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-sm-mx,), 275 | ); 276 | 277 | $height-vw: ( 278 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,)), 279 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-lg-mx,), 280 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-md-mx,), 281 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-sm-mx,), 282 | ); 283 | 284 | $height-perc: ( 285 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,)), 286 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-lg-mx,), 287 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-md-mx,), 288 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-sm-mx,), 289 | ); 290 | 291 | // list-style ls 292 | $listStyle: ( 293 | (vals: ('n': none,)), 294 | ); 295 | 296 | // margin m, margin-top mt, margin-bottom mb, margin-left ml, margin-right mr 297 | $margin: ( 298 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,)), 299 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-lg-mx,), 300 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-md-mx,), 301 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-sm-mx,), 302 | ); 303 | 304 | $margin-vw: ( 305 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,)), 306 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-lg-mx,), 307 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-md-mx,), 308 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-sm-mx,), 309 | ); 310 | 311 | $margin-perc: ( 312 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,)), 313 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-lg-mx,), 314 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-md-mx,), 315 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-sm-mx,), 316 | ); 317 | 318 | // opacity op 319 | $opacity: ( 320 | (vals: ('0': 0, '\\.1': .1, '\\.2': .2, '\\.3': .3, '\\.4': .4, '\\.5': .5, '\\.6': .6, '\\.7': .7, '\\.8': .8, '\\.9': .9, '1': 1,),), 321 | ); 322 | 323 | // overflow o, overflow-x ox, overflow-y oy 324 | $overflow: ( 325 | (vals: ('h': hidden, 'a': auto, 's': scroll),), 326 | ); 327 | 328 | 329 | // padding p, padding-top pt, padding-bottom pb, padding-left pl, padding-right pr 330 | $padding: ( 331 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,)), 332 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-lg-mx,), 333 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-md-mx,), 334 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-sm-mx,), 335 | ); 336 | 337 | $padding-vw: ( 338 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,)), 339 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-lg-mx,), 340 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-md-mx,), 341 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-sm-mx,), 342 | ); 343 | 344 | $padding-perc: ( 345 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,)), 346 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-lg-mx,), 347 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-md-mx,), 348 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-sm-mx,), 349 | ); 350 | 351 | // position pos 352 | $position: ( 353 | (vals: ('s': static, 'r': relative, 'a': absolute, 'f': fixed)), 354 | (vals: ('s': static, 'r': relative, 'a': absolute, 'f': fixed), bp: $bp-lg-mx,), 355 | (vals: ('s': static, 'r': relative, 'a': absolute, 'f': fixed), bp: $bp-md-mx,), 356 | (vals: ('s': static, 'r': relative, 'a': absolute, 'f': fixed), bp: $bp-sm-mx,), 357 | ); 358 | 359 | //******************** 360 | // svg 361 | //******************** 362 | 363 | // stroke stroke 364 | $stroke : ( 365 | (vals: ( 366 | 'black' : $color-black, 367 | 'white' : $color-white, 368 | 'lightgray': $color-lightgray, 369 | 'gray' : $color-gray, 370 | )), 371 | ); 372 | 373 | // fill fill 374 | $fill : ( 375 | (vals: ( 376 | 'black' : $color-black, 377 | 'white' : $color-white, 378 | 'lightgray': $color-lightgray, 379 | 'gray' : $color-gray, 380 | )), 381 | ); 382 | //******************** 383 | // end svg 384 | //******************** 385 | 386 | //******************** 387 | // text 388 | //******************** 389 | 390 | // text-align ta 391 | $textAlign: ( 392 | (vals: ('l': left, 'c': center, 'r': right, 'j': justify),), 393 | (vals: ('l': left, 'c': center, 'r': right, 'j': justify), bp: $bp-lg-mx,), 394 | (vals: ('l': left, 'c': center, 'r': right, 'j': justify), bp: $bp-md-mx,), 395 | (vals: ('l': left, 'c': center, 'r': right, 'j': justify), bp: $bp-sm-mx,), 396 | ); 397 | 398 | // text-decoration td 399 | $textDecoration: ( 400 | (vals: ('n': none, 'u': underline),), 401 | ); 402 | 403 | //text-shadow txsh 404 | $textShadow: ( 405 | (vals: ( 406 | '1-1-1-white' : 1px 1px 1px $color-white, 407 | '1-1-1-lightgray': 1px 1px 1px $color-lightgray, 408 | '1-1-1-gray' : 1px 1px 1px $color-gray, 409 | '1-1-1-black' : 1px 1px 1px $color-black, 410 | ),), 411 | ); 412 | 413 | // text-transform tt 414 | $textTransform: ( 415 | (vals: ('u': uppercase, 'n': none),), 416 | ); 417 | 418 | // line-height lh 419 | $lineHeight: ( 420 | (vals: ('1': 1, '1\\.2': 1.2, '1\\.3': 1.3, '1\\.4': 1.4, '1\\.5': 1.5, '2': 2,)), 421 | ); 422 | //******************** 423 | // end text 424 | //******************** 425 | 426 | /// transform trf 427 | $transforms: ( 428 | (vals: ('rz45': rotateZ(45deg), 'rz-45': rotateZ(-45deg), 'rz90': rotateZ(90deg), 'rz-90': rotateZ(-90deg), 'rz180': rotateZ(180deg))), 429 | ); 430 | 431 | /// transition trs 432 | $transitions: ( 433 | (vals: ('bgc-0\\.3s': background-color 0.3s, 'c-0\\.3s': color 0.3s, 'all-0\\.3s': all 0.3s, )), 434 | ); 435 | 436 | // width w 437 | $width: ( 438 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,)), 439 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-lg-mx,), 440 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-md-mx,), 441 | (vals: ('0': 0, '1': 1px, '5': 5px, '10': 10px, '15': 15px, '20': 20px, '25': 25px, '30': 30px, '40': 40px, '44': 44px, '50': 50px, '60': 60px, '70': 70px, '75': 75px, '80': 80px, '90': 90px, '100': 100px, '125': 125px, '150': 150px, '175': 175px, '200': 200px, '225': 225px, '250': 250px, '275': 275px, '300': 300px, '325': 325px, '350': 350px, '375': 375px, '400': 400px, '500': 500px, '600': 600px, '700': 700px, '800': 800px,), bp: $bp-sm-mx,), 442 | ); 443 | 444 | $width-vw: ( 445 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,)), 446 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-lg-mx,), 447 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-md-mx,), 448 | (vals: ('0': 0vw, '1': 1vw, '5': 5vw, '10': 10vw, '15': 15vw, '20': 20vw, '25': 25vw, '30': 30vw, '40': 40vw, '44': 44vw, '50': 50vw, '60': 60vw, '70': 70vw, '75': 75vw, '80': 80vw, '90': 90vw, '100': 100vw, '150': 150vw, '200': 200vw,), bp: $bp-sm-mx,), 449 | ); 450 | 451 | $width-perc: ( 452 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,)), 453 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-lg-mx,), 454 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-md-mx,), 455 | (vals: ('1': 1%, '2': 2%, '3': 3%, '4': 4%, '5': 5%, '10': 10%, '12\\.25': 12.25%, '15': 15%, '20': 20%, '25': 25%, '33': 30%, '33\\.3333': 33.3333%, '35': 35%, '40': 40%, '45': 45%, '50': 50%, '55': 55%, '60': 60%, '65': 65%, '66\\.6666': 66.6666%, '70': 70%, '75': 75%, '80': 80%, '85': 85%, '90': 90%, '95': 95%, '100': 100%, '110': 110%, '120': 120%, '130': 130%, '140': 140%, '150': 150%,), bp: $bp-sm-mx,), 456 | ); 457 | 458 | // vertical-align va 459 | $verticalAlign: ( 460 | (vals: ('t': top, 'b': bottom, 'm': middle, 'sup': super, 'sub': sub, 'base': base,),), 461 | ); 462 | 463 | // word-break wob 464 | $wordBreak: ( 465 | (vals: ('n': normal, 'k': keep-all, 'ba': break-all, 'nm': normal,),), 466 | ); 467 | 468 | // word-wrap wow 469 | $wordWrap: ( 470 | (vals: ('u': unrestricted, 's': suppress, 'b': break-word, 'n': none,),), 471 | ); 472 | 473 | //z-index z 474 | $zIndex: ( 475 | (vals: ('1': 1,'2': 2,'3': 3,'4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, '99': 99,),), 476 | ); 477 | --------------------------------------------------------------------------------