├── 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 |
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 |
9 |
10 |
11 |
12 |
13 |
14 | {isBrowser && window.__USER &&
15 |
Keystone
16 | }
17 |
18 |
19 |
20 |
21 |
22 |
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 |
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 |
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 |
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 |
48 | { labelText }
49 |
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 |
38 | {labelText}
39 |
40 | }
41 | this.input = input}
45 | onChange={() => getFieldChanged({ [fieldName + 'Value']: this.input.value })}
46 | onBlur={() => setFieldDirty({[fieldName + 'Dirty']: true})}
47 | {...disabled ? { disabled: true } : {}}
48 | value={fieldValue}
49 | >
50 | {children}
51 |
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. Close
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 Open Lightbox
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 |
this.input.focus()}
42 | htmlFor={fieldName}
43 | className="input-textarea__label fz16"
44 | >
45 | {labelText}
46 |
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 |
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 |
67 | { props.abbrStateDisplay ? key : states[key] }
68 |
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 |
64 |
65 |
66 |
67 | {labelText && labelText }
68 | {renderLabel && renderLabel() }
69 |
70 |
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 | this.input.focus()}
59 | htmlFor={fieldName}
60 | className="input-text__label fz16"
61 | >
62 | {labelText}
63 |
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 |
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 |
--------------------------------------------------------------------------------