├── src ├── utils │ ├── cache.js │ ├── router.js │ ├── cache-crawler.js │ ├── store.js │ └── shared.js ├── svgs │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── logo.svg │ └── logo-funkhaus.svg ├── views │ ├── FrontPage.vue │ ├── Default.vue │ └── Archive.vue ├── components │ ├── SiteHeader.vue │ ├── gutenberg │ │ └── Content.vue │ └── SvgImage.vue ├── styles │ ├── _base.scss │ ├── _transitions.scss │ ├── _vars.scss │ └── _easings.scss ├── main.js └── App.vue ├── blocks ├── dist │ ├── blocks.css │ └── blocks.js ├── src │ ├── _index.scss │ ├── prebuilt │ │ ├── index.js │ │ ├── text.js │ │ └── image.js │ ├── index.js │ └── utils │ │ └── builder.js ├── .babelrc └── blocks.config.js ├── screenshot.png ├── static ├── images │ ├── favicon.png │ ├── icon-touch.png │ └── logo.svg ├── fonts │ └── fonts.css ├── css │ ├── admin.css │ └── login.css └── js │ └── admin.js ├── acf ├── acf-imports.php └── locations │ └── page-grandparent.php ├── backstop_data ├── engine_scripts │ └── chromy │ │ ├── onBefore.js │ │ ├── onReady.js │ │ └── clickAndHoverHelper.js └── bitmaps_reference │ ├── VUEPRESS_Front_Page_0_document_1_phone.png │ └── VUEPRESS_Front_Page_0_document_0_desktop.png ├── style.css ├── .gitignore ├── functions ├── custom-vuehaus-templates.php ├── images.php ├── blocks.php ├── shortcodes.php ├── router.php ├── vh-functions.php ├── meta-boxes.php ├── README.md ├── wp-functions.php ├── rest-easy-filters.php └── developer-role.php ├── parts ├── font-loader.php ├── ga-tracking.php ├── og-tags.php └── schema.php ├── LICENSE ├── backstop.json ├── index.php ├── functions.php ├── webpack.config.js ├── package.json └── README.md /src/utils/cache.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /blocks/dist/blocks.css: -------------------------------------------------------------------------------- 1 | .fh-custom-block { 2 | color: #000000; 3 | } 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkhaus/vuehaus/HEAD/screenshot.png -------------------------------------------------------------------------------- /static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkhaus/vuehaus/HEAD/static/images/favicon.png -------------------------------------------------------------------------------- /static/images/icon-touch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkhaus/vuehaus/HEAD/static/images/icon-touch.png -------------------------------------------------------------------------------- /blocks/src/_index.scss: -------------------------------------------------------------------------------- 1 | @import 'src/styles/vars'; 2 | 3 | .fh-custom-block { 4 | color: $black; 5 | } 6 | -------------------------------------------------------------------------------- /blocks/src/prebuilt/index.js: -------------------------------------------------------------------------------- 1 | import text from './text' 2 | import image from './image' 3 | 4 | export default { 5 | text, 6 | image 7 | } 8 | -------------------------------------------------------------------------------- /acf/acf-imports.php: -------------------------------------------------------------------------------- 1 | ' + scenario.label) 3 | require('./clickAndHoverHelper')(chromy, scenario) 4 | } 5 | -------------------------------------------------------------------------------- /src/svgs/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/svgs/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/FrontPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: Vuehaus 3 | Theme URI: http://funkhaus.us 4 | Description: A Vue theme for WordPress. Depends on "Rest Easy" plugin by Funkhaus to properly work. 5 | Author: Funkhaus 6 | Author URI: http://www.funkhaus.us 7 | Text Domain: vuehaus 8 | Version: 2.0.0 9 | */ 10 | -------------------------------------------------------------------------------- /src/components/SiteHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/views/Default.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # build/bundle files 6 | static/build* 7 | static/bundle* 8 | *.map 9 | bundleStats.json 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | node_modules 17 | .DS_Store 18 | .deploy.config.js 19 | .deployrc.json 20 | 21 | # backstop testing 22 | backstop_data/* 23 | !backstop_data/bitmaps_reference 24 | !backstop_data/engine_scripts 25 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | @import 'src/styles/vars'; 2 | 3 | body { 4 | background-color: $white; 5 | margin: 0; 6 | -webkit-font-smoothing: antialiased !important; 7 | text-rendering: optimizeLegibility !important; 8 | } 9 | button { 10 | appearance: none; 11 | border: none; 12 | background-color: transparent; 13 | cursor: pointer; 14 | outline: none; 15 | margin: 0; 16 | } 17 | -------------------------------------------------------------------------------- /blocks/src/index.js: -------------------------------------------------------------------------------- 1 | import buildBlock from './utils/builder' 2 | 3 | // Example block 4 | // buildBlock({ 5 | // name: 'Title and Image', 6 | // slug: 'title-and-image', 7 | // description: 'A title and an image.', 8 | // class: 'title-and-image', 9 | // content: [ 10 | //

Title

, 11 | // { name: 'content', type: 'text' }, 12 | //

Image

, 13 | // { name: 'image', type: 'image' } 14 | // ] 15 | // }) 16 | -------------------------------------------------------------------------------- /functions/custom-vuehaus-templates.php: -------------------------------------------------------------------------------- 1 | 2 | WebFontConfig = { 3 | // google: { 4 | // families: [] 5 | // }, 6 | // typekit: { 7 | // id: '' 8 | // }, 9 | // custom: { 10 | // families: [], 11 | // urls: ['/static/fonts/fonts.css'] 12 | // } 13 | }; 14 | 15 | (function(d) { 16 | var wf = d.createElement('script'), s = d.scripts[0]; 17 | wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js'; 18 | wf.async = true; 19 | s.parentNode.insertBefore(wf, s); 20 | })(document); 21 | 22 | -------------------------------------------------------------------------------- /backstop_data/engine_scripts/chromy/clickAndHoverHelper.js: -------------------------------------------------------------------------------- 1 | module.exports = function(chromy, scenario) { 2 | var hoverSelector = scenario.hoverSelector 3 | var clickSelector = scenario.clickSelector 4 | var postInteractionWait = scenario.postInteractionWait // selector [str] | ms [int] 5 | 6 | if (hoverSelector) { 7 | chromy 8 | .wait(hoverSelector) 9 | .rect(hoverSelector) 10 | .result(function(rect) { 11 | chromy.mouseMoved(rect.left, rect.top) 12 | }) 13 | } 14 | 15 | if (clickSelector) { 16 | chromy.wait(clickSelector).click(clickSelector) 17 | } 18 | 19 | if (postInteractionWait) { 20 | chromy.wait(postInteractionWait) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /static/fonts/fonts.css: -------------------------------------------------------------------------------- 1 | /* 2 | Load fonts in this file using @font-face 3 | Example: 4 | @font-face { 5 | font-family: 'FontName'; 6 | src: url('FontName.eot'); 7 | src: url('FontName.eot?#iefix') format('embedded-opentype'), 8 | url('FontName.woff2') format('woff2'), 9 | url('FontName.woff') format('woff'), 10 | url('FontName.ttf') format('truetype'); 11 | font-style: normal; 12 | font-weight: 400; 13 | } 14 | 15 | Font Weights: 16 | 100 - Thin 17 | 200 - Extra Light (Ultra Light) 18 | 300 - Light 19 | 400 - Normal 20 | 500 - Medium 21 | 600 - Semi Bold (Demi Bold) 22 | 700 - Bold 23 | 800 - Extra Bold (Ultra Bold) 24 | 900 - Black (Heavy) 25 | */ 26 | 27 | -------------------------------------------------------------------------------- /blocks/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": [ 9 | "last 2 Chrome versions", 10 | "last 2 Firefox versions", 11 | "last 2 Safari versions", 12 | "last 2 iOS versions", 13 | "last 1 Android version", 14 | "last 1 ChromeAndroid version", 15 | "ie 11" 16 | ] 17 | } 18 | } 19 | ] 20 | ], 21 | "plugins": [ 22 | [ 23 | "transform-react-jsx", 24 | { 25 | "pragma": "wp.element.createElement" 26 | } 27 | ], 28 | "transform-object-rest-spread" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /parts/ga-tracking.php: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 24 | 25 | -------------------------------------------------------------------------------- /src/styles/_transitions.scss: -------------------------------------------------------------------------------- 1 | // Fade 2 | .fade-enter, 3 | .fade-leave-to { 4 | opacity: 0; 5 | } 6 | .fade-enter-active, 7 | .fade-leave-active { 8 | transition: opacity 0.4s; 9 | } 10 | 11 | // Slide left & slide right 12 | .slide-right-enter, 13 | .slide-left-leave-to { 14 | transform: translateX(-100%); 15 | } 16 | .slide-left-enter, 17 | .slide-right-leave-to { 18 | transform: translateX(100%); 19 | } 20 | .slide-right-enter-active, 21 | .slide-right-leave-active, 22 | .slide-left-enter-active, 23 | .slide-left-leave-active { 24 | transition: transform 0.4s; 25 | } 26 | 27 | // Slide up & slide down 28 | .slide-up-enter, 29 | .slide-down-leave-to { 30 | transform: translateY(100%); 31 | } 32 | .slide-down-enter, 33 | .slide-up-leave-to { 34 | transform: translateY(-100%); 35 | } 36 | .slide-up-enter-active, 37 | .slide-up-leave-active, 38 | .slide-down-enter-active, 39 | .slide-down-leave-active { 40 | transition: transform 0.4s; 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/_vars.scss: -------------------------------------------------------------------------------- 1 | // Sass constants 2 | $white: #ffffff; 3 | $black: #000000; 4 | $font-family: 'Helvetica'; 5 | $desktop-padding: 50px; 6 | $mobile-padding: 20px; 7 | $header-height: 80px; 8 | 9 | // Breakpoints 10 | $gt-cinema: 'only screen and (min-width: 1800px)'; 11 | $lt-desktop: 'only screen and (max-width: 1100px)'; 12 | $lt-phone: 'only screen and (max-width: 750px)'; 13 | $lt-phone-landscape: 'only screen and (max-width: 750px) and (orientation: landscape)'; 14 | $ipad: 'only screen and (min-device-width : 768px) and (max-device-width : 1024px)'; 15 | 16 | // mixins 17 | @mixin fill($position: absolute) { 18 | position: $position; 19 | bottom: 0; 20 | right: 0; 21 | left: 0; 22 | top: 0; 23 | } 24 | @mixin cover { 25 | background-repeat: no-repeat; 26 | background-position: center; 27 | background-size: cover; 28 | } 29 | @mixin contain { 30 | background-repeat: no-repeat; 31 | background-position: center; 32 | background-size: contain; 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Funkhaus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backstop.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "VUEPRESS", 3 | "viewports": [ 4 | { 5 | "label": "desktop", 6 | "width": 1200, 7 | "height": 672 8 | }, 9 | { 10 | "label": "phone", 11 | "width": 320, 12 | "height": 480 13 | } 14 | ], 15 | "onBeforeScript": "chromy/onBefore.js", 16 | "onReadyScript": "chromy/onReady.js", 17 | "readySelector": ".container", 18 | "scenarios": [ 19 | { 20 | "label": "Front Page", 21 | "url": "http://example.local" 22 | } 23 | ], 24 | "paths": { 25 | "bitmaps_reference": "backstop_data/bitmaps_reference", 26 | "bitmaps_test": "backstop_data/bitmaps_test", 27 | "engine_scripts": "backstop_data/engine_scripts", 28 | "html_report": "backstop_data/html_report", 29 | "ci_report": "backstop_data/ci_report" 30 | }, 31 | "misMatchThreshold": 1.0, 32 | "report": ["browser"], 33 | "engine": "chrome", 34 | "asyncCaptureLimit": 5, 35 | "asyncCompareLimit": 50, 36 | "debug": false, 37 | "debugWindow": false 38 | } 39 | -------------------------------------------------------------------------------- /blocks/src/prebuilt/text.js: -------------------------------------------------------------------------------- 1 | const { RichText } = wp.editor 2 | 3 | export default { 4 | attributes: child => { 5 | return { 6 | type: 'array', 7 | source: 'children', 8 | selector: `.${child.name}` 9 | } 10 | }, 11 | edit: (props, child) => { 12 | const { attributes, className, setAttributes } = props 13 | const content = attributes[child.name] 14 | 15 | const classes = `${child.name} ${className}` 16 | 17 | function onChangeContent(newContent) { 18 | setAttributes({ [child.name]: newContent }) 19 | } 20 | 21 | return ( 22 | 28 | ) 29 | }, 30 | save: (props, child) => { 31 | // data attributes passed from editor 32 | const { attributes } = props 33 | 34 | // get content and classes relevant to this particular block 35 | const content = attributes[child.name] 36 | const classes = `${child.name}` 37 | 38 | return ( 39 | 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/views/Archive.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 54 | 55 | 57 | -------------------------------------------------------------------------------- /blocks/blocks.config.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 2 | let webpack = require('webpack'), 3 | NODE_ENV = process.env.NODE_ENV || 'development', 4 | webpackConfig = { 5 | entry: ['./blocks/src/index.js', './blocks/src/_index.scss'], 6 | output: { 7 | path: __dirname, 8 | filename: './dist/blocks.js' 9 | }, 10 | module: { 11 | loaders: [ 12 | { 13 | test: /.js$/, 14 | loader: 'babel-loader', 15 | exclude: /node_modules/ 16 | }, 17 | { 18 | test: /.scss$/, 19 | loader: ExtractTextPlugin.extract({ 20 | fallbackLoader: 'style-loader', 21 | loader: 'css-loader!sass-loader' 22 | }) 23 | } 24 | ] 25 | }, 26 | plugins: [ 27 | new webpack.DefinePlugin({ 28 | 'process.env.NODE_ENV': JSON.stringify(NODE_ENV) 29 | }), 30 | new ExtractTextPlugin('./dist/blocks.css') 31 | ] 32 | } 33 | if ('production' === NODE_ENV) { 34 | webpackConfig.plugins.push(new webpack.optimize.UglifyJsPlugin()) 35 | } 36 | 37 | module.exports = webpackConfig 38 | -------------------------------------------------------------------------------- /src/styles/_easings.scss: -------------------------------------------------------------------------------- 1 | $easeInSine: cubic-bezier(0.47, 0, 0.745, 0.715); 2 | $easeOutSine: cubic-bezier(0.39, 0.575, 0.565, 1); 3 | $easeInOutSine: cubic-bezier(0.39, 0.575, 0.565, 1); 4 | 5 | $easeInQuad: cubic-bezier(0.55, 0.085, 0.68, 0.53); 6 | $easeOutQuad: cubic-bezier(0.25, 0.46, 0.45, 0.94); 7 | $easeInOutQuad: cubic-bezier(0.25, 0.46, 0.45, 0.94); 8 | 9 | $easeInCubic: cubic-bezier(0.55, 0.055, 0.675, 0.19); 10 | $easeOutCubic: cubic-bezier(0.215, 0.61, 0.355, 1); 11 | $easeInOutCubic: cubic-bezier(0.215, 0.61, 0.355, 1); 12 | 13 | $easeInQuart: cubic-bezier(0.895, 0.03, 0.685, 0.22); 14 | $easeOutQuart: cubic-bezier(0.165, 0.84, 0.44, 1); 15 | $easeInOutQuart: cubic-bezier(0.165, 0.84, 0.44, 1); 16 | 17 | $easeInQuint: cubic-bezier(0.755, 0.05, 0.855, 0.06); 18 | $easeOutQuint: cubic-bezier(0.23, 1, 0.32, 1); 19 | $easeInOutQuint: cubic-bezier(0.23, 1, 0.32, 1); 20 | 21 | $easeInExpo: cubic-bezier(0.95, 0.05, 0.795, 0.035); 22 | $easeOutExpo: cubic-bezier(0.19, 1, 0.22, 1); 23 | $easeInOutExpo: cubic-bezier(0.19, 1, 0.22, 1); 24 | 25 | $easeInCirc: cubic-bezier(0.6, 0.04, 0.98, 0.335); 26 | $easeOutCirc: cubic-bezier(0.075, 0.82, 0.165, 1); 27 | $easeInOutCirc: cubic-bezier(0.075, 0.82, 0.165, 1); 28 | 29 | $easeInBack: cubic-bezier(0.6, -0.28, 0.735, 0.045); 30 | $easeOutBack: cubic-bezier(0.175, 0.885, 0.32, 1.275); 31 | $easeInOutBack: cubic-bezier(0.68, -0.55, 0.265, 1.55); 32 | 33 | $easeInOutFast: cubic-bezier(1, 0, 0, 1); 34 | 35 | $authenticMotion: cubic-bezier(0.4, 0, 0.2, 1); 36 | -------------------------------------------------------------------------------- /functions/blocks.php: -------------------------------------------------------------------------------- 1 | 'vuehaus-block-style', 27 | 'editor_script' => 'vuehaus-block-registration', 28 | ) 29 | ); 30 | 31 | // Add this theme to the custom block categories 32 | add_filter( 'block_categories', function( $categories ) { 33 | return array_merge( 34 | $categories, 35 | array( 36 | array( 37 | 'slug' => 'custom-fh', 38 | 'title' => __( get_bloginfo('name') ), 39 | ), 40 | ) 41 | ); 42 | }, 10, 2 ); 43 | } 44 | add_action('admin_init', 'register_vuehaus_blocks'); 45 | -------------------------------------------------------------------------------- /src/components/gutenberg/Content.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 57 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | prefix="og: http://ogp.me/ns#"> 12 | 13 | 14 | <?php wp_title(); ?> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | > 31 | 32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 | '' 14 | ), $atts)); 15 | 16 | // Props to pass to Vue component 17 | $props = 'title="' . $title . '"'; 18 | $content = custom_filter_shortcode_text($content); 19 | 20 | return ''. $content .''; 21 | } 22 | //add_shortcode( 'shortcode-name', 'custom_shortcode_function' ); 23 | 24 | 25 | /* 26 | * Creates a an [svg-image] shortcode so a user can add SVGs into the editor correctly 27 | */ 28 | function add_svg_image_shortcode( $atts ) { 29 | 30 | extract(shortcode_atts(array( 31 | 'src' => '' 32 | ), $atts)); 33 | 34 | $props = 'src="' . $src . '"'; 35 | 36 | return ''; 37 | } 38 | //add_shortcode( 'svg-image', 'add_svg_image_shortcode' ); 39 | 40 | 41 | /** 42 | * Utility function to clean up the way WordPress auto formats text in a shortcode. 43 | * 44 | * @param string $text A stirng of HTML text 45 | */ 46 | function custom_filter_shortcode_text($text = '') { 47 | // Replace all the poorly formatted P tags that WP adds by default. 48 | $tags = array("

", "

"); 49 | $text = str_replace($tags, "\n", $text); 50 | 51 | // Remove any BR tags 52 | $tags = array("
", "
", "
"); 53 | $text = str_replace($tags, "", $text); 54 | 55 | // Add back in the P and BR tags again, this will remove empty ones 56 | return apply_filters('the_content', $text); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/SvgImage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 70 | -------------------------------------------------------------------------------- /src/svgs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /blocks/src/utils/builder.js: -------------------------------------------------------------------------------- 1 | // prep prebuilt components 2 | import prebuilt from '../prebuilt' 3 | 4 | const { registerBlockType } = wp.blocks 5 | const { RichText } = wp.editor 6 | 7 | export default (opts = {}) => { 8 | const settings = { 9 | // defaults 10 | slug: 'block-slug', 11 | name: 'Example', 12 | description: '', 13 | icon: 'format-gallery', 14 | content: [], 15 | class: 'block-class', 16 | 17 | // passed values 18 | ...opts 19 | } 20 | 21 | // build attributes according to content children 22 | const attributes = {} 23 | settings.content.map(child => { 24 | if (child.type && prebuilt[child.type]) { 25 | attributes[child.name] = prebuilt[child.type].attributes(child) 26 | } 27 | }) 28 | 29 | // register desired block 30 | registerBlockType(`custom-fh/${settings.slug}`, { 31 | title: settings.name, 32 | description: settings.description, 33 | icon: settings.icon, 34 | category: 'custom-fh', 35 | 36 | // use attributes defined above 37 | attributes, 38 | 39 | // Editor 40 | edit(props) { 41 | const { attributes, className, setAttributes } = props 42 | const classes = `${settings.class} fh-custom-block` 43 | 44 | const output = settings.content 45 | .map(child => { 46 | if (child.type && prebuilt[child.type]) { 47 | return prebuilt[child.type].edit(props, child) 48 | } 49 | 50 | return child 51 | }) 52 | .filter(x => x) 53 | 54 | return
{output}
55 | }, 56 | 57 | // On save 58 | save(props) { 59 | const output = settings.content 60 | .map(child => { 61 | if (child.type && prebuilt[child.type]) { 62 | return prebuilt[child.type].save(props, child) 63 | } 64 | 65 | return null 66 | }) 67 | .filter(x => x) 68 | 69 | return
{output}
70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /parts/og-tags.php: -------------------------------------------------------------------------------- 1 | post_content) ); 28 | } 29 | } 30 | 31 | // Remove any links, tags or line breaks from summary 32 | $summary = strip_tags($summary); 33 | $summary = esc_attr($summary); 34 | $summary = preg_replace('!\s+!', ' ', $summary); 35 | 36 | /* 37 | * Build permalink URL 38 | */ 39 | $url = get_permalink($post->ID); 40 | if( is_front_page() ) { 41 | $url = get_bloginfo('url'); 42 | } 43 | ?> 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /acf/locations/page-grandparent.php: -------------------------------------------------------------------------------- 1 | 'page' 18 | )); 19 | if (!empty($groups)) { 20 | foreach(array_keys($groups) as $group_title) { 21 | $posts = acf_extract_var($groups, $group_title); 22 | foreach(array_keys($posts) as $post_id) { 23 | $posts[$post_id] = acf_get_post_title($posts[$post_id]); 24 | }; 25 | $choices = $posts; 26 | } 27 | } 28 | // end of copy from ACF 29 | return $choices; 30 | } 31 | add_filter('acf/location/rule_types', 'acf_location_rules_page_grandparent'); 32 | 33 | 34 | function acf_location_rules_match_page_grandparent($match, $rule, $options) { 35 | // this code is with inspiration from 36 | // acf_location::rule_match_page_parent() 37 | // with addition of adding grandparent check 38 | if(!$options['post_id']) { 39 | return false; 40 | } 41 | $post_grandparent = 0; 42 | $post = get_post($options['post_id']); 43 | if ($post->post_parent) { 44 | $parent = get_post($post->post_parent); 45 | if ($parent->post_parent) { 46 | $post_grandparent = $parent->post_parent; 47 | } 48 | } 49 | if (isset($options['page_parent']) && $options['page_parent']) { 50 | $parent = get_post($options['page_parent']); 51 | if ($parent->post_parent) { 52 | $post_grandparent = $parent->post_parent; 53 | } 54 | } 55 | if (!$post_grandparent) { 56 | return false; 57 | } 58 | if ($rule['operator'] == "==") { 59 | $match = ($post_grandparent == $rule['value']); 60 | } elseif ($rule['operator'] == "!=") { 61 | $match = ($post_grandparent != $rule['value']); 62 | } 63 | return $match; 64 | } 65 | add_filter('acf/location/rule_match/page_grandparent', 'acf_location_rules_match_page_grandparent', 10, 3); 66 | -------------------------------------------------------------------------------- /static/css/admin.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Style Dashboard 3 | * By Drew Baker - 2011 4 | * drew@funkhuasdesign.com 5 | * 6 | */ 7 | 8 | /* 9 | * Logo Icon 10 | */ 11 | /* This should be 16px X 16px */ 12 | #wpwrap #wpadminbar #wp-admin-bar-wp-logo > .ab-item { 13 | background-image: url(../images/favicon.png); 14 | background-position: center center; 15 | background-repeat: no-repeat; 16 | background-size: 26px auto; 17 | background-color: #ffffff; 18 | } 19 | #wpwrap #wpadminbar #wp-admin-bar-wp-logo > .ab-item span { 20 | visibility: hidden; 21 | } 22 | body:not(.is-developer) #toplevel_page_edit-post_type-acf-field-group { 23 | display: none; 24 | } 25 | 26 | /* 27 | * Post/Page Meta Boxs 28 | */ 29 | .custom-meta label { 30 | width: 280px; 31 | display: inline-block; 32 | text-align: right; 33 | } 34 | .custom-meta input { 35 | width: calc(100% - 320px); 36 | max-width: 800px; 37 | height: 25px; 38 | vertical-align: middle; 39 | } 40 | .custom-meta .for-textarea { 41 | vertical-align: top; 42 | } 43 | .custom-meta textarea { 44 | width: calc(100% - 320px); 45 | max-width: 800px; 46 | } 47 | .custom-meta input[type='checkbox'], 48 | .custom-meta input[type='radio'] { 49 | height: 16px; 50 | width: 16px; 51 | display: inline-block; 52 | vertical-align: middle; 53 | } 54 | /* Image Uploader */ 55 | .custom-meta .custom-image-container img { 56 | width: 100%; 57 | height: auto; 58 | } 59 | .custom-meta.custom-image-uploader p:last-child { 60 | margin-bottom: 1px; 61 | } 62 | 63 | /* 64 | * Turn off menus 65 | */ 66 | /* 67 | #menu-posts { 68 | display: none; 69 | } 70 | #menu-media { 71 | display: none; 72 | } 73 | #menu-links { 74 | display: none; 75 | } 76 | #menu-pages { 77 | display: none; 78 | } 79 | #menu-appearance { 80 | display: none; 81 | } 82 | #menu-plugins { 83 | display: none; 84 | } 85 | #menu-users { 86 | display: none; 87 | } 88 | #menu-tools { 89 | display: none; 90 | } 91 | #menu-settings { 92 | display: none; 93 | } 94 | */ 95 | #menu-comments { 96 | display: none; 97 | } 98 | /* body:not(.is-developer) #toplevel_page_edit-post_type-acf-field-group { 99 | display: none; 100 | } */ 101 | -------------------------------------------------------------------------------- /functions/router.php: -------------------------------------------------------------------------------- 1 | 'VueComponent', 18 | // '/path/:var' => 'ComponentWithVar' 19 | // '/path/*/:var' => 'WildcardAndVar' 20 | // path_from_dev_id('dev-id') => 'DefinedByDevId', 21 | // path_from_dev_id('dev-id', '/append-me') => 'DevIdPathPlusAppendedString', 22 | 23 | /* 24 | // Common Funkhaus setups 25 | // Directors - redirect to first child 26 | path_from_dev_id('directors') => array( 27 | 'redirect' => get_child_of_dev_id_path('directors') 28 | ), 29 | // Director detail 30 | path_from_dev_id('directors', '/:director') => 'DirectorDetail', 31 | // Reel grid 32 | path_from_dev_id('directors', '/:director/:reel') => 'ReelGrid', 33 | // Video detail 34 | path_from_dev_id('directors', '/:director/:reel/:video') => 'VideoDetail', 35 | // About 36 | path_from_dev_id('about') => 'About', 37 | // Contact 38 | path_from_dev_id('contact') => 'Contact', 39 | // Contact region - redirect to parent 40 | path_from_dev_id('contact', '/:region') => array( 41 | 'redirect' => path_from_dev_id('contact') 42 | ), 43 | */ 44 | 45 | // Probably unchanging 46 | '' => 'FrontPage', 47 | '/' . $category_base => 'Archive', 48 | '/(\\d+)/:slug' => array( 49 | 'name' => 'SinglePost', 50 | 'component' => 'Default' 51 | ), // If Permalinks set to "/%post_id%/%postname%/" then this will be a single Blog post 52 | '*' => array( 53 | 'name' => 'Fallback', 54 | 'component' => 'Default' 55 | ) 56 | 57 | ); 58 | 59 | $jsonData['routes'] = array_merge( get_custom_template_routes(), $programmed_routes ); 60 | 61 | return $jsonData; 62 | } 63 | add_filter('rez_build_all_data', 'add_routes_to_json'); 64 | -------------------------------------------------------------------------------- /src/utils/router.js: -------------------------------------------------------------------------------- 1 | /* global jsonData, _get */ 2 | 3 | import VueRouter from 'vue-router' 4 | import store from 'src/utils/store' 5 | import Vue from 'vue' 6 | 7 | Vue.use(VueRouter) 8 | 9 | // load all templates in folder 10 | const templates = require.context('src/views', true) 11 | 12 | // build routing table 13 | const routeTable = [] 14 | Object.keys(jsonData.routes).forEach(path => { 15 | let routeObject = jsonData.routes[path] 16 | 17 | if (typeof routeObject === 'string') { 18 | routeObject = { 19 | component: routeObject 20 | } 21 | } 22 | 23 | routeObject.path = path 24 | 25 | routeObject.name = routeObject.name || routeObject.component 26 | 27 | // get specified component, fallback to default 28 | let component = templates(`./Default.vue`) 29 | if (templates.keys().indexOf(`./${routeObject.component}.vue`) > -1) { 30 | component = templates(`./${routeObject.component}.vue`) 31 | } 32 | 33 | routeObject.component = component 34 | 35 | // push new route entry to table 36 | routeTable.push(routeObject) 37 | }) 38 | 39 | // these two values will determine where we'll scroll on our new page 40 | let targetScroll = null 41 | let savedScroll = null 42 | 43 | const router = new VueRouter({ 44 | mode: 'history', 45 | routes: routeTable, 46 | scrollBehavior() { 47 | return targetScroll || { x: 0, y: 0 } 48 | } 49 | }) 50 | 51 | router.beforeEach((to, from, next) => { 52 | // Check to see if we're going from page A to B to A again 53 | const firstPagePath = _get(store, 'state.referral.path', '') 54 | const toPath = _get(to, 'path', '') 55 | 56 | if (firstPagePath && toPath && firstPagePath == toPath) { 57 | // if we are, make our target scroll the position we saved from the first time on A 58 | targetScroll = savedScroll 59 | } else { 60 | // if we're not, default to the top of the page 61 | targetScroll = { x: 0, y: 0 } 62 | } 63 | 64 | // Save scroll from previous page 65 | savedScroll = { x: window.scrollX, y: window.scrollY } 66 | 67 | // Replace query data 68 | if (to.path !== from.path) { 69 | store.dispatch('LOAD_AND_REPLACE_QUERYDATA', { path: to.path }) 70 | } 71 | 72 | // Update referral route 73 | if (from.name !== null) { 74 | store.commit('UPDATE_REFERRAL_ROUTE', from) 75 | } 76 | next() 77 | }) 78 | 79 | export default router 80 | -------------------------------------------------------------------------------- /blocks/src/prebuilt/image.js: -------------------------------------------------------------------------------- 1 | const { MediaUpload } = wp.editor 2 | const { Button } = wp.components 3 | 4 | export default { 5 | // attributes 6 | attributes: child => { 7 | return { 8 | mediaID: { 9 | type: 'number' 10 | }, 11 | mediaURL: { 12 | type: 'string', 13 | source: 'attribute', 14 | selector: 'img', 15 | attribute: 'src' 16 | }, 17 | width: { 18 | type: 'number' 19 | }, 20 | height: { 21 | type: 'number' 22 | } 23 | } 24 | }, 25 | 26 | // editing 27 | edit: (props, child) => { 28 | const { setAttributes, attributes } = props 29 | 30 | // try to find correct attributes, default to empty object 31 | const atts = attributes[child.name] || {} 32 | const { mediaID, mediaURL } = atts 33 | 34 | // selecting the image 35 | function onSelectImage(newImage) { 36 | // console.log(newImage) 37 | setAttributes({ 38 | [child.name]: { 39 | mediaURL: newImage.url, 40 | mediaID: newImage.id, 41 | width: newImage.width, 42 | height: newImage.height 43 | } 44 | }) 45 | } 46 | 47 | // edit block 48 | return ( 49 | ( 54 | 62 | )} 63 | /> 64 | ) 65 | }, 66 | save: (props, child) => { 67 | const { attributes } = props 68 | 69 | // ignore if we don't have the correct content 70 | if (!attributes || !attributes[child.name]) { 71 | return '' 72 | } 73 | 74 | const { mediaID, mediaURL, width, height } = attributes[child.name] 75 | 76 | if (mediaURL) { 77 | return ( 78 | 85 | ) 86 | } 87 | 88 | return '' 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/utils/cache-crawler.js: -------------------------------------------------------------------------------- 1 | import cache from 'src/utils/cache' 2 | import store from 'src/utils/store' 3 | import _get from 'lodash/get' 4 | 5 | // flatten n-dimensional array 6 | // https://stackoverflow.com/a/15030117/3856675 7 | function flatten(arr) { 8 | return arr.reduce(function(flat, toFlatten) { 9 | return flat.concat( 10 | Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten 11 | ) 12 | }, []) 13 | } 14 | 15 | class CacheCrawler { 16 | constructor() { 17 | this.candidates = [] 18 | this.fetching = false 19 | this.index = 0 20 | } 21 | 22 | onNewPage() { 23 | // reset index 24 | this.index = 0 25 | 26 | // links in all site menus 27 | const menus = _get(store, 'state.site.menus', []) 28 | const menuCandidates = menus.map(menu => 29 | menu.items.map(item => (item.isExternal ? null : item.relativePath)) 30 | ) 31 | // links in the loop 32 | const loop = _get(store, 'state.loop', []) 33 | const loopCandidates = loop.map(page => { 34 | const output = [page.relativePath] 35 | if (page.related.children && page.related.children.length) { 36 | output.push( 37 | page.related.children.map(child => child.relativePath) 38 | ) 39 | } 40 | return output 41 | }) 42 | 43 | // list of locations where we want to look for links 44 | const candidateQueue = [menuCandidates, loopCandidates] 45 | this.candidates = flatten(candidateQueue) 46 | 47 | if (!this.fetching) { 48 | this.goFetch() 49 | } 50 | } 51 | 52 | async goFetch() { 53 | this.fetching = true 54 | 55 | if (this.index > this.candidates.length - 1) { 56 | // we've cycled through all available candidates, so exit for now 57 | this.fetching = false 58 | return 59 | } 60 | 61 | const path = this.candidates[this.index++] 62 | 63 | // conditions when we should ignore this path 64 | const ignorePath = 65 | !path || 66 | typeof path != 'string' || 67 | !path.length || 68 | path.startsWith('#') 69 | 70 | if (!ignorePath && !cache[path]) { 71 | // Allows for query strings in path 72 | const connector = path.includes('?') ? '&' : '?' 73 | 74 | const response = await fetch( 75 | `${path}${connector}contentType=json`, 76 | { 77 | credentials: 'same-origin' 78 | } 79 | ).then(res => (res.ok ? res.json() : false)) 80 | 81 | // avoid caching non-200 responses 82 | if (response) { 83 | cache[path] = response 84 | } 85 | } 86 | 87 | this.goFetch() 88 | } 89 | } 90 | 91 | export default new CacheCrawler() 92 | -------------------------------------------------------------------------------- /static/css/login.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Style login page 3 | * By Drew Baker - 2011 4 | * drew@funkhausdesign.com 5 | * 6 | */ 7 | 8 | /* 9 | * Import fonts 10 | */ 11 | /* @import url("../fonts/fonts.css"); */ 12 | 13 | /* 14 | * Globals 15 | */ 16 | :root { 17 | --main-color: white; 18 | --accent-color: black; 19 | --text-color: black; 20 | --hover-text-color: #00a0d2; 21 | } 22 | html { 23 | background: var(--main-color); 24 | } 25 | body.login { 26 | font-size: 14px; 27 | background: var(--main-color); 28 | line-height: 1.4; 29 | padding: 0; 30 | margin: 0; 31 | border: none; 32 | width: 100%; 33 | } 34 | a { 35 | outline: none; 36 | } 37 | 38 | /* 39 | * Login Banner Image 40 | */ 41 | #login h1 a { 42 | background-image: url(../images/logo.svg); 43 | background-position: center center; 44 | margin: 0; 45 | background-size: auto; 46 | width: auto; 47 | } 48 | 49 | /* 50 | * Login Form 51 | */ 52 | #loginform { 53 | box-shadow: none; 54 | background: var(--main-color); 55 | } 56 | #loginform label { 57 | color: var(--text-color); 58 | } 59 | #loginform input[type='text'], 60 | #loginform input[type='password'] { 61 | background: #f2f2f2; 62 | border: 1px solid #cccccc; 63 | font-family: inherit; 64 | padding: 5px 8px; 65 | box-sizing: border-box; 66 | } 67 | #loginform input[type='checkbox'], 68 | #loginform input[type='radio'] { 69 | vertical-align: baseline; 70 | } 71 | 72 | /* 73 | * Login Submit Button 74 | */ 75 | #wp-submit { 76 | background: var(--accent-color); 77 | color: var(--main-color); 78 | border: none; 79 | text-shadow: none; 80 | box-shadow: none; 81 | text-transform: uppercase; 82 | border: 2px solid var(--accent-color); 83 | height: initial; 84 | padding-top: 2px; 85 | } 86 | #wp-submit:hover, 87 | #wp-submit:focus { 88 | background: var(--main-color); 89 | color: var(--accent-color); 90 | border: 2px solid var(--text-color); 91 | } 92 | 93 | /* 94 | * "Lost password" link 95 | */ 96 | #nav { 97 | text-shadow: none; 98 | } 99 | .login #nav a { 100 | text-decoration: none; 101 | padding: 0 1px; 102 | text-shadow: none; 103 | color: var(--text-color); 104 | text-decoration: underline; 105 | } 106 | .login #nav a:hover, 107 | .login #nav a:focus { 108 | color: var(--hover-text-color) !important; 109 | font-weight: normal; 110 | text-shadow: none; 111 | } 112 | 113 | /* 114 | * "Back to blog" link header 115 | */ 116 | #backtoblog { 117 | display: none; 118 | } 119 | 120 | .login .message, 121 | .login #login_error { 122 | background-color: var(--main-color); 123 | color: var(--text-color); 124 | } 125 | .login #login_error a { 126 | color: var(--text-color); 127 | } 128 | .login #login_error a:hover, 129 | .login #login_error a:focus { 130 | color: var(--hover-text-color); 131 | } 132 | -------------------------------------------------------------------------------- /src/utils/store.js: -------------------------------------------------------------------------------- 1 | /* global jsonData */ 2 | import Vuex from 'vuex' 3 | import Vue from 'vue' 4 | import cache from 'src/utils/cache' 5 | import _get from 'lodash/get' 6 | import CacheCrawler from 'src/utils/cache-crawler' 7 | 8 | // add vuex 9 | Vue.use(Vuex) 10 | 11 | export default new Vuex.Store({ 12 | state: { 13 | site: jsonData.site, 14 | meta: jsonData.meta, 15 | loop: jsonData.loop, 16 | loaded: true, 17 | menuOpened: false, 18 | referral: null, 19 | currentlyFetching: null 20 | }, 21 | mutations: { 22 | REPLACE_QUERYDATA: (state, data) => { 23 | state.site = data.site 24 | state.meta = data.meta 25 | state.loop = data.loop 26 | 27 | // reboot cache-crawler 28 | CacheCrawler.onNewPage() 29 | 30 | return state 31 | }, 32 | SET_LOADED: (state, loaded) => { 33 | state.loaded = loaded || false 34 | }, 35 | OPEN_MENU: state => { 36 | state.menuOpened = true 37 | }, 38 | CLOSE_MENU: state => { 39 | state.menuOpened = false 40 | }, 41 | UPDATE_REFERRAL_ROUTE: (state, referral) => { 42 | state.referral = referral 43 | }, 44 | SET_CURRENTLY_FETCHING: (state, { path }) => { 45 | state.currentlyFetching = path 46 | } 47 | }, 48 | actions: { 49 | LOAD_AND_REPLACE_QUERYDATA: async (context, payload) => { 50 | const path = payload.path || '' 51 | 52 | // flag that we're looking for the given path 53 | context.commit('SET_CURRENTLY_FETCHING', { path }) 54 | 55 | // no cache? set it 56 | if (!cache[payload.path]) { 57 | context.commit('SET_LOADED', false) 58 | cache[path] = fetch(`${path}?contentType=json`, { 59 | credentials: 'same-origin' 60 | }).then(r => r.json()) 61 | } 62 | 63 | // wait for data 64 | const data = await cache[path] 65 | 66 | // if we're still waiting for the data from this path, replace it 67 | if (context.state.currentlyFetching == path) { 68 | context.commit('REPLACE_QUERYDATA', data) 69 | } 70 | 71 | // notify that we're finished loading 72 | context.commit('SET_LOADED', true) 73 | } 74 | }, 75 | getters: { 76 | loading: state => { 77 | return !state.loaded 78 | }, 79 | post: state => { 80 | // Return an empty object if we're still loading 81 | if (!state.loaded) { 82 | return {} 83 | } 84 | 85 | // This is a "post" in the sense of a WordPress post - the first result of the Loop 86 | return _get(state.loop, '[0]', {}) 87 | }, 88 | referralPath: state => { 89 | return _get(state.referral, 'fullPath', '') 90 | } 91 | } 92 | }) 93 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /* global jsonData */ 2 | import App from './App.vue' 3 | import Vue from 'vue' 4 | import cache from 'src/utils/cache' 5 | import _kebabCase from 'lodash/kebabCase' 6 | 7 | // Register components in src/ 8 | // =============================== 9 | const components = require.context('src/components', true) 10 | components.keys().map(component => { 11 | // turn './ComponentName.vue' into 'component-name' 12 | const componentName = _kebabCase( 13 | component.replace(/^\.\//, '').replace(/\.vue$/, '') 14 | ) 15 | // register new component globally 16 | Vue.component(componentName, components(component)) 17 | }) 18 | 19 | // Register outside components 20 | // =============================== 21 | 22 | // Register fh-components 23 | // Vue.component('a-div', require('fh-components/a-div')) 24 | // Vue.component('flex-text', require('fh-components/flex-text')) 25 | Vue.component('hamburger-button', require('fh-components/hamburger-button')) 26 | Vue.component('image-loader', require('fh-components/image-loader')) 27 | Vue.component('load-on-view', require('fh-components/load-on-view')) 28 | // Vue.component('mailing-list', require('fh-components/mailing-list')) 29 | // Vue.component('menu-item', require('fh-components/wp-menu-item')) 30 | Vue.component('responsive-image', require('fh-components/responsive-image')) 31 | // Vue.component('reveal-footer', require('fh-components/reveal-footer')) 32 | // Vue.component('scroll-to', require('fh-components/scroll-to')) 33 | // Vue.component('slide-show', require('fh-components/slide-show')) 34 | // Vue.component('split-text', require('fh-components/split-text')) 35 | Vue.component('svg-image', require('src/components/SvgImage.vue')) 36 | // Vue.component('sticky-wrap', require('fh-components/sticky-wrap')) 37 | // Vue.component('text-typer', require('fh-components/text-typer')) 38 | // Vue.component('velocity-animate', require('fh-components/velocity-animate')) 39 | // Vue.component('video-stage', require('fh-components/video-stage')) 40 | Vue.component('wp-content', require('fh-components/wp-content')) 41 | Vue.component('wp-menu', require('fh-components/wp-menu')) 42 | 43 | // Register directives 44 | // import animated from 'fh-components/v-animated' 45 | // Vue.directive('animated', animated) 46 | // import draggable from 'fh-components/v-draggable' 47 | // Vue.directive('draggable', draggable) 48 | // import fullHeight from 'fh-components/v-full-height' 49 | // Vue.directive('full-height', fullHeight) 50 | // import inView from 'fh-components/v-in-view' 51 | // Vue.directive('in-view', inView) 52 | // import interact from 'fh-components/v-interact' 53 | // Vue.directive('interact', interact) 54 | // import keyDown from 'fh-components/v-keydown' 55 | // Vue.directive('keydown', keyDown) 56 | // import reverseHover from 'fh-components/v-reverse-hover' 57 | // Vue.directive('reverse-hover', reverseHover) 58 | 59 | // save initial page cache - dumped onto page as `jsonData` by Rest-Easy 60 | cache[window.location.pathname] = Promise.resolve(jsonData) 61 | 62 | // boot app 63 | new Vue(App) 64 | -------------------------------------------------------------------------------- /src/svgs/logo-funkhaus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 99 | 100 | 105 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 3 | const resolve = file => path.resolve(__dirname, file) 4 | const webpack = require('webpack') 5 | const path = require('path') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | const config = { 10 | entry: ['whatwg-fetch', 'string.prototype.includes', './src/main'], 11 | output: { 12 | path: resolve('static'), 13 | filename: isProd ? 'bundle.js' : 'bundle.dev.js' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.vue$/, 19 | loader: 'vue-loader', 20 | options: { 21 | loaders: { 22 | scss: ExtractTextPlugin.extract({ 23 | use: isProd 24 | ? 'css-loader?minimize&url=false!sass-loader?minimize' 25 | : 'css-loader?{"sourceMap":true}&url=false!sass-loader?{"sourceMap":true}', 26 | fallback: 'vue-style-loader' 27 | }) 28 | }, 29 | extractCSS: true, 30 | preserveWhitespace: false, 31 | postcss: [ 32 | require('autoprefixer')({ 33 | browsers: ['last 5 versions'] 34 | }) 35 | ] 36 | } 37 | }, 38 | { 39 | test: /\.js$/, 40 | loader: 'babel-loader', 41 | exclude: /node_modules/ 42 | }, 43 | { 44 | test: /\.svg$/, 45 | loader: 'svg-inline-loader?removeSVGTagAttrs=false' 46 | }, 47 | { 48 | test: /\.(png|woff|woff2|eot|ttf)$/, 49 | loader: 'url-loader', 50 | options: { 51 | limit: 10000, 52 | name: '[name].[ext]?[hash]' 53 | } 54 | }, 55 | { 56 | test: /\.css$/, 57 | use: ExtractTextPlugin.extract({ 58 | use: isProd 59 | ? 'css-loader?minimize&url=false' 60 | : 'css-loader?url=false', 61 | fallback: 'vue-style-loader' 62 | }) 63 | } 64 | ] 65 | }, 66 | resolve: { 67 | modules: [ 68 | path.resolve(__dirname, 'app'), 69 | resolve('./'), 70 | 'node_modules' 71 | ], 72 | alias: { 73 | vue$: isProd ? 'vue/dist/vue.min.js' : 'vue/dist/vue.esm.js' 74 | } 75 | }, 76 | performance: { 77 | maxEntrypointSize: 500000, 78 | maxAssetSize: 500000, 79 | hints: isProd ? 'warning' : false 80 | }, 81 | plugins: [ 82 | new ExtractTextPlugin({ 83 | filename: 'bundle.css' 84 | }), 85 | new webpack.ProvidePlugin({ 86 | _get: ['lodash/get'] 87 | }) 88 | ] 89 | } 90 | 91 | // production only 92 | if (isProd) { 93 | config.plugins = config.plugins.concat([new UglifyJsPlugin()]) 94 | 95 | // dev only 96 | } else { 97 | config.devtool = '#source-map' 98 | config.plugins = config.plugins.concat([ 99 | new webpack.EnvironmentPlugin({ 100 | NODE_ENV: 'development' 101 | }) 102 | ]) 103 | } 104 | 105 | module.exports = config 106 | -------------------------------------------------------------------------------- /functions/vh-functions.php: -------------------------------------------------------------------------------- 1 | 'vuehaus', // Unique ID for hashing notices for multiple instances of TGMPA. 15 | 'default_path' => '', // Default absolute path to bundled plugins. 16 | 'menu' => 'tgmpa-install-plugins', // Menu slug. 17 | 'parent_slug' => 'themes.php', // Parent menu slug. 18 | 'capability' => 'edit_theme_options', // Capability needed to view plugin install page, should be a capability associated with the parent menu used. 19 | 'has_notices' => true, // Show admin notices or not. 20 | 'dismissable' => true, // If false, a user cannot dismiss the nag message. 21 | 'dismiss_msg' => '', // If 'dismissable' is false, this message will be output at top of nag. 22 | 'is_automatic' => false, // Automatically activate plugins after installation or not. 23 | 'message' => '', // Message to output right before the plugins table. 24 | ); 25 | 26 | $plugins = array( 27 | array( 28 | 'name' => 'Rest-Easy', 29 | 'slug' => 'rest-easy', 30 | 'source' => 'https://github.com/funkhaus/Rest-Easy/archive/master.zip', 31 | 'version' => $latest_rest_easy 32 | ), 33 | array( 34 | 'name' => 'Nested Pages', 35 | 'slug' => 'wp-nested-pages', 36 | 'required' => false 37 | ), 38 | array( 39 | 'name' => 'Focushaus', 40 | 'slug' => 'focushaus', 41 | 'source' => 'https://github.com/funkhaus/focushaus/archive/master.zip', 42 | 'version' => $latest_focushaus 43 | ) 44 | ); 45 | 46 | tgmpa( $plugins, $config ); 47 | } 48 | 49 | // Register required & recommended plugins 50 | add_action( 'tgmpa_register', 'vuehaus_register_required_plugins' ); 51 | 52 | 53 | function get_custom_template_routes(){ 54 | 55 | // Find all pages with custom_vuehaus_template set 56 | $args = array( 57 | 'posts_per_page' => -1, 58 | 'orderby' => 'menu_order', 59 | 'order' => 'ASC', 60 | 'meta_key' => 'custom_vuehaus_template', 61 | 'meta_value' => '', 62 | 'post_type' => array('post', 'page') 63 | ); 64 | $all_pages_with_custom_templates = get_posts($args); 65 | 66 | // Filter out pages with vuehaus custom template set to Default 67 | $filtered_pages_with_custom_templates = array_filter( 68 | $all_pages_with_custom_templates, 69 | function( $page ){ 70 | return $page->custom_vuehaus_template !== 'Default'; 71 | } 72 | ); 73 | 74 | // Build out router for pages with custom templates 75 | $custom_template_routes = array(); 76 | foreach( $filtered_pages_with_custom_templates as $page ){ 77 | $page_url = get_permalink($page); 78 | $custom_template_routes[wp_make_link_relative($page_url)] = $page->custom_vuehaus_template; 79 | } 80 | 81 | return $custom_template_routes; 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuehaus", 3 | "version": "3.0.0", 4 | "description": "Build smooth, responsive Wordpress sites with Vue.", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "dev": "npm run clean && webpack --watch", 8 | "build": "npm run clean && cross-env NODE_ENV=production webpack", 9 | "test": "backstop test", 10 | "clean": "rimraf static/bundle.*", 11 | "deploy": "fh-deploy", 12 | "format": "prettier --write 'src/**/*+(.js|.vue|.scss)'", 13 | "analyze": "webpack --profile --json > bundleStats.json && webpack-bundle-analyzer bundleStats.json", 14 | "precommit": "pretty-quick --staged", 15 | "block-dev": "cross-env BABEL_ENV=default webpack --config blocks/blocks.config.js --watch", 16 | "block-build": "cross-env BABEL_ENV=default NODE_ENV=production webpack --config blocks/blocks.config.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/funkhaus/vuepress.git" 21 | }, 22 | "babel": { 23 | "presets": [ 24 | "env", 25 | "stage-0" 26 | ], 27 | "plugins": [ 28 | "transform-runtime" 29 | ] 30 | }, 31 | "prettier": { 32 | "tabWidth": 4, 33 | "semi": false, 34 | "singleQuote": true 35 | }, 36 | "files": [ 37 | "blocks/**/*.*", 38 | "functions/**/*.*", 39 | "parts/**/*.*", 40 | "static/**/*.*", 41 | "functions.php", 42 | "index.php", 43 | "style.css", 44 | "screenshot.png", 45 | "screenshot.jpg" 46 | ], 47 | "author": "Funkhaus", 48 | "license": "ISC", 49 | "dependencies": { 50 | "auto-blur": "^1.1.1", 51 | "case": "^1.5.4", 52 | "colors": "^1.1.2", 53 | "fh-components": "^1.2.90", 54 | "imagesloaded": "^4.1.3", 55 | "jquery": "^3.1.1", 56 | "lodash": "^4.17.4", 57 | "scroll-to-element": "^2.0.0", 58 | "string.prototype.includes": "^1.0.0", 59 | "velocity-animate": "^1.5.0", 60 | "vue": "^2.5.13", 61 | "vue-loader": "^11.1.4", 62 | "vue-router": "^2.8.1", 63 | "vuex": "^2.5.0", 64 | "whatwg-fetch": "^2.0.3" 65 | }, 66 | "devDependencies": { 67 | "babel-core": "^6.26.0", 68 | "babel-loader": "^6.2.10", 69 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 70 | "babel-plugin-transform-react-jsx": "^6.24.1", 71 | "babel-plugin-transform-runtime": "^6.15.0", 72 | "babel-preset-env": "^1.6.1", 73 | "babel-preset-stage-0": "^6.24.1", 74 | "backstopjs": "^3.8.4", 75 | "compression-webpack-plugin": "^2.0.0", 76 | "cross-env": "^5.2.0", 77 | "css-loader": "^0.26.4", 78 | "extract-text-webpack-plugin": "^2.1.0", 79 | "fh-deploy": "^1.1.5", 80 | "file-loader": "^0.9.0", 81 | "fs-extra": "^5.0.0", 82 | "husky": "^0.14.3", 83 | "node-sass": "^4.7.2", 84 | "optimize-css-assets-webpack-plugin": "^2.0.0", 85 | "postcss-import": "^9.1.0", 86 | "postcss-loader": "^1.3.3", 87 | "postcss-url": "^7.3.0", 88 | "prettier": "^1.14.3", 89 | "pretty-quick": "^1.4.1", 90 | "rimraf": "^2.6.2", 91 | "sass-loader": "^6.0.6", 92 | "style-loader": "^0.13.1", 93 | "svg-inline-loader": "^0.7.1", 94 | "uglifyjs-webpack-plugin": "^1.2.5", 95 | "url-loader": "^1.1.2", 96 | "vue-template-compiler": "^2.5.13", 97 | "webpack": "^2.2.1", 98 | "webpack-bundle-analyzer": "^2.11.1" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /parts/schema.php: -------------------------------------------------------------------------------- 1 | 'http://schema.org', 12 | '@type' => 'WebPage', 13 | 'headline' => get_bloginfo('description'), 14 | 'url' => get_bloginfo('url'), 15 | 'thumbnailUrl' => get_template_directory_uri() . '/screenshot.' . $extension 16 | ); 17 | 18 | // helper to generate schema Person object 19 | function schema_generate_person($name){ 20 | return array( 21 | '@type' => 'Person', 22 | 'name' => $name 23 | ); 24 | } 25 | 26 | // helper to generate schema Organization object 27 | function schema_generate_org($name = null, $logo = null){ 28 | return array( 29 | '@type' => 'Organization', 30 | 'name' => $name ? $name : get_bloginfo('name'), 31 | 'logo' => $logo ? $logo : get_template_directory_uri() . '/images/logo.svg' 32 | ); 33 | } 34 | 35 | // set some helpful vars 36 | $image_id = false; 37 | $image_url = false; 38 | $image = wp_get_attachment_image_src($image_id, 'social-preview'); 39 | if( $image ){ 40 | $image_id = get_post_thumbnail_id( $post ); 41 | $image_url = reset( $image ); 42 | } 43 | $author = get_user_by('ID', $post->post_author); 44 | $queried_object = get_queried_object(); 45 | $excerpt = get_the_excerpt($post); 46 | 47 | // build on schema conditionally 48 | switch (true) { 49 | 50 | // is non-home page 51 | case !is_front_page() and is_page(): 52 | $schema['headline'] = get_the_title($post); 53 | $schema['url'] = get_the_permalink($post); 54 | $schema['thumbnailUrl'] = $image_url; 55 | $schema['description'] = $excerpt ? $excerpt : get_bloginfo('description'); 56 | break; 57 | 58 | // is single post 59 | case is_single(): 60 | $schema['@type'] = 'NewsArticle'; 61 | $schema['headline'] = get_the_title($post); 62 | $schema['url'] = get_the_permalink($post); 63 | $schema['thumbnailUrl'] = $image_url; 64 | $schema['image'] = $image_url; 65 | $schema['dateCreated'] = get_the_date('Y-m-d'); 66 | $schema['datePublished'] = get_the_date('Y-m-d'); 67 | $schema['articleSection'] = ''; 68 | $schema['description'] = get_the_excerpt($post); 69 | $schema['creator'] = $author ? schema_generate_person($author->display_name) : schema_generate_org(); 70 | $schema['author'] = $author ? schema_generate_person($author->display_name) : schema_generate_org(); 71 | $schema['publisher'] = schema_generate_org(); 72 | $schema['keywords'] = wp_get_post_categories($post->ID, array( 73 | 'fields' => 'names' 74 | )); 75 | break; 76 | 77 | // all archive pages 78 | case is_archive(): 79 | $schema['@type'] = 'Blog'; 80 | 81 | // is a term archive 82 | if ( property_exists($queried_object, 'term_id') ){ 83 | $schema['headline'] = term_description($queried_object->term_id); 84 | $schema['url'] = get_term_link($queried_object->term_id); 85 | $schema['articleSection'] = $queried_object->name; 86 | } 87 | break; 88 | 89 | // blog archive, if different than front page 90 | case is_home() and !is_front_page(): 91 | $schema['@type'] = 'Blog'; 92 | $schema['url'] = get_the_permalink(get_option('page_for_posts')); 93 | break; 94 | 95 | } 96 | 97 | ?> 98 | 99 | 100 | 103 | 104 | -------------------------------------------------------------------------------- /static/js/admin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var funkhausAdmin = { 3 | 4 | init: function() { 5 | funkhausAdmin.showNavMenuUrls(); 6 | funkhausAdmin.showAttachmentIds(); 7 | funkhausAdmin.autoRefresh(); 8 | //funkhausAdmin.secondFeaturedImageUploader(); 9 | }, 10 | 11 | showNavMenuUrls: function(){ 12 | 13 | // Show the URL of menu items when hovering over wp_nav_menu box 14 | var url; 15 | jQuery('#nav-menu-meta').on('mouseenter', 'label.menu-item-title', function(){ 16 | // On mouse in 17 | url = jQuery(this).siblings('input.menu-item-url').val(); 18 | jQuery(this).attr('title', url); 19 | } 20 | ); 21 | 22 | }, 23 | 24 | showAttachmentIds: function(){ 25 | 26 | // Show the attachment URLs on hover of attachment grid blocks 27 | jQuery(document).on('mouseenter', '.media-modal .attachment, .media-frame .attachment', function(){ 28 | var id = jQuery(this).data('id'); 29 | if(id) { 30 | jQuery(this).attr('title', 'Attachment ID: ' + id); 31 | } 32 | }); 33 | 34 | } 35 | 36 | autoRefresh: function(){ 37 | 38 | // In the admin backend when a new parent is selected from the dropdown, automatically trigger 'save draft' 39 | jQuery('.wp-admin #pageparentdiv #parent_id').change(function(e){ 40 | funkhausAdmin.saveDraft(); 41 | }); 42 | 43 | }, 44 | 45 | saveDraft: function(){ 46 | var $count = 0; 47 | 48 | // Save Draft post after 500ms 49 | function loopCount(){ 50 | if ( !jQuery('#save-action input[type="submit"]').is(':disabled') ) { 51 | jQuery('#save-action input[type="submit"]').trigger('click'); 52 | } else if ($count < 10) { 53 | setTimeout(function() { 54 | $count++; 55 | loopCount(); 56 | }, 500); 57 | } 58 | } 59 | loopCount(); 60 | }, 61 | 62 | secondFeaturedImageUploader: function(){ 63 | // Cache vars to stay in scope 64 | var frame; 65 | var $metaBox = jQuery('.custom-image-uploader'); 66 | var $addImgLink = $metaBox.find('.upload-custom-image'); 67 | var $delImgLink = $metaBox.find('.delete-custom-image'); 68 | var $imgContainer = $metaBox.find('.custom-image-container'); 69 | var $imgIdInput = $metaBox.find('.custom-image-id'); 70 | 71 | // ADD IMAGE LINK 72 | $addImgLink.on( 'click', function(e){ 73 | 74 | e.preventDefault(); 75 | 76 | // If the media frame already exists, reopen it. 77 | if ( frame ) { 78 | frame.open(); 79 | return; 80 | } 81 | 82 | // Create a new media frame 83 | frame = wp.media({ 84 | title: 'Select image to use', 85 | button: { 86 | text: 'Use this image' 87 | }, 88 | multiple: false // Set to true to allow multiple files to be selected 89 | }); 90 | 91 | // What to do when image is selected in the media frame, and the save button is clicked 92 | frame.on('select', function() { 93 | 94 | // Get media attachment details from the frame state 95 | var attachment = frame.state().get('selection').first().toJSON(); 96 | 97 | // Send the attachment URL to our custom image input field. 98 | $imgContainer.append(''); 99 | 100 | // Send the attachment id to our hidden input 101 | $imgIdInput.val(attachment.id); 102 | 103 | // Hide the add image link 104 | $addImgLink.addClass('hidden'); 105 | 106 | // Unhide the remove image link 107 | $delImgLink.removeClass('hidden'); 108 | }); 109 | 110 | // Finally, open the modal on click 111 | frame.open(); 112 | }); 113 | 114 | 115 | // DELETE IMAGE LINK 116 | $delImgLink.on('click', function(e){ 117 | 118 | e.preventDefault(); 119 | 120 | // Clear out the preview image 121 | $imgContainer.html(''); 122 | 123 | // Un-hide the add image link 124 | $addImgLink.removeClass('hidden'); 125 | 126 | // Hide the delete image link 127 | $delImgLink.addClass('hidden'); 128 | 129 | // Delete the image id from the hidden input 130 | $imgIdInput.val(''); 131 | 132 | }); 133 | } 134 | }; 135 | jQuery(document).ready(function(){ 136 | funkhausAdmin.init(); 137 | }); 138 | -------------------------------------------------------------------------------- /src/utils/shared.js: -------------------------------------------------------------------------------- 1 | import scrollToElement from 'scroll-to-element' 2 | 3 | /* global _get */ 4 | 5 | /* 6 | * This file is used to house small pure utilities that we use frequesntly. 7 | * You can import these from anywhere in the src folder like this: 8 | * import { utilOne, utilTwo } from 'src/utils/shared' 9 | */ 10 | 11 | // Description: Take array of items and sort equally across n new arrays 12 | // -- Returns: sorted buckets (array of arrays) 13 | export const sortBuckets = (items, count = 2) => { 14 | const buckets = Array(count) 15 | .fill(0) 16 | .map(v => []) 17 | let pointer = 0 18 | 19 | items.forEach(item => { 20 | buckets[pointer].push(item) 21 | pointer = (pointer + 1) % count 22 | }) 23 | 24 | return buckets 25 | } 26 | 27 | // Description: Easily preload an image by URL or full attachment object 28 | // -- Returns: Promise that resolves to image DOM element 29 | export const preloadImg = (img, size = 'full') => { 30 | return new Promise((resolve, reject) => { 31 | // assume URL provided 32 | let src = img 33 | 34 | // if object provided, assume it's 35 | // a rest-easy serialized attachment 36 | if (typeof img === 'object') { 37 | src = _get(img, `sizes[${size}].url`, '') 38 | } 39 | 40 | // ensure we have a URL and an image constructor 41 | if (!src || !Image) return reject() 42 | 43 | // make image object and set callbacks 44 | const imageEl = new Image() 45 | imageEl.onload = () => resolve(imageEl) 46 | imageEl.onerror = reject 47 | return (imageEl.src = src) 48 | }) 49 | } 50 | 51 | // Description: Easily preload a video file by URL or full attachment object 52 | // -- Returns: Promise that resolves to video DOM element 53 | export const preloadVid = vid => { 54 | return new Promise((resolve, reject) => { 55 | // assume URL provided 56 | let src = vid 57 | 58 | // if object provided, assume it's 59 | // a rest-easy serialized attachment 60 | if (typeof vid === 'object') { 61 | src = _get(vid, `meta.custom_video_url`) || '' 62 | } 63 | 64 | // ensure we have a URL and a document object 65 | if (!src || !document) return reject() 66 | 67 | // make DOM element and set callbacks 68 | const vidEl = document.createElement('video') 69 | vidEl.addEventListener('canplay', () => resolve(vidEl)) 70 | vidEl.addEventListener('error', reject) 71 | return (vidEl.src = src) 72 | }) 73 | } 74 | 75 | // Description: Tiny helper function to promise-ify a timeout 76 | // -- Returns: Promise 77 | export const wait = (time = 1000) => { 78 | return new Promise(resolve => { 79 | return setTimeout(resolve, time) 80 | }) 81 | } 82 | 83 | export const buildShareLinks = opts => { 84 | const url = opts.url || '' 85 | const text = opts.text || '' 86 | const title = opts.title || '' 87 | 88 | return { 89 | facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent( 90 | url 91 | )}`, 92 | twitter: `http://twitter.com/share?text=${encodeURIComponent( 93 | text.substring(0, 280) 94 | )}&url=${encodeURIComponent(url)}`, 95 | tumblr: `http://www.tumblr.com/share/link?url=${encodeURIComponent( 96 | url 97 | )}`, 98 | reddit: `http://www.reddit.com/submit?url=${url}&title=${encodeURIComponent( 99 | title 100 | )}`, 101 | email: `mailto:?subject=${encodeURIComponent( 102 | title 103 | )}&body=${encodeURIComponent(text)}%0D%0A %0D%0A${encodeURIComponent( 104 | url 105 | )}`, 106 | linkedin: `http://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent( 107 | url 108 | )}&title=${encodeURIComponent(title)}&summary=${encodeURIComponent( 109 | text 110 | )}`, 111 | pinterest: `http://pinterest.com/pin/create/button/?url=${encodeURIComponent( 112 | url 113 | )}&description=${encodeURIComponent(text)}`, 114 | reddit: `http://www.reddit.com/submit?url=${encodeURIComponent(url)}` 115 | } 116 | } 117 | 118 | // smooth scroll to top of page 119 | export const scrollUp = () => { 120 | return new Promise((resolve, reject) => { 121 | const sTop = window.pageYOffset || document.documentElement.scrollTop 122 | if (sTop <= 10) return resolve() 123 | 124 | const speed = Math.min(sTop, 500) 125 | const scroller = scrollToElement(document.body, { 126 | duration: speed, 127 | ease: 'inOutQuad' 128 | }) 129 | if (scroller) scroller.on('end', resolve) 130 | else reject() 131 | }) 132 | } 133 | 134 | // clamp a value between min and max, inclusive 135 | export const clamp = (val, min, max) => { 136 | return Math.min(Math.max(val, min), max) 137 | } 138 | 139 | // mimics PHP's nl2br 140 | export const nl2br = str => { 141 | return str.replace(/(?:\r\n|\r|\n)/g, '
') 142 | } 143 | 144 | // wait for an element with the given CSS selector to appear: 145 | // await asyncWaitFor('.thing') 146 | // doSomethingWith('.thing') 147 | export const asyncWaitFor = item => { 148 | return new Promise(res => { 149 | let present = false 150 | 151 | // set up loop 152 | const check = () => { 153 | present = document.querySelector(item) 154 | 155 | if (!present) { 156 | requestAnimationFrame(check) 157 | } else { 158 | requestAnimationFrame(res) 159 | } 160 | } 161 | 162 | // kick off loop 163 | check() 164 | }) 165 | } 166 | 167 | -------------------------------------------------------------------------------- /functions/meta-boxes.php: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 | 23 | 1 ) : ?> 24 | 25 | 26 | 31 |
32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 | ID ) ); 56 | 57 | // See if there's a media id already saved as post meta 58 | $image_id = get_post_meta( $post->ID, $meta_key, true ); 59 | 60 | // Get the image src 61 | $image_src = wp_get_attachment_image_src( $image_id, 'post-thumbnail' ); 62 | 63 | // For convenience, see if the array is valid 64 | $has_image = is_array( $image_src ); 65 | 66 | ?> 67 | 68 |
69 | 70 | 71 | 72 | 73 | 74 |
75 | 76 | 77 | 78 |
79 | 80 | 81 |

82 | 83 | 84 | 85 | 86 | 87 | 88 |

89 | 90 |
91 | 92 | ID, 'custom_video_url', true ); 124 | $fields['custom_video_url'] = array( 125 | 'label' => __( 'Video URL', 'text-domain' ), 126 | 'input' => 'text', 127 | 'value' => $meta, 128 | 'show_in_edit' => true, 129 | ); 130 | 131 | return $fields; 132 | } 133 | add_filter( 'attachment_fields_to_edit', 'custom_add_attachment_video_url', 10, 2 ); 134 | 135 | 136 | /* 137 | * Update media custom field when updating attachment 138 | */ 139 | function custom_update_attachment_video_url( $post_id = null ) { 140 | if( empty($post_id) and isset($_POST['id']) ) { 141 | $post_id = $_POST['id']; 142 | } 143 | 144 | if( isset( $_POST['attachments'][ $post_id ]['custom_video_url'] ) ) { 145 | update_post_meta( $post_id , 'custom_video_url', $_POST['attachments'][ $post_id ]['custom_video_url'] ); 146 | } 147 | 148 | clean_post_cache($post_id); 149 | return; 150 | } 151 | add_action( 'edit_attachment', 'custom_update_attachment_video_url', 1 ); 152 | add_action( 'wp_ajax_save-attachment-compat', 'custom_update_attachment_video_url', 0, 1 ); 153 | -------------------------------------------------------------------------------- /functions/README.md: -------------------------------------------------------------------------------- 1 | ## Example Rest-Easy Filters 2 | 3 | Here are a bunch of useful examples of how to use the Rest-Easy filter functions to send custom data to the frontend. 4 | 5 | ```php 6 | /** 7 | * Include the second post thumbnail in all posts 8 | * 9 | * @param array $post_data The post currently being processed by Rest-Easy 10 | */ 11 | function add_second_featured_image($post_data){ 12 | $image_id = get_post_meta($post_data['id'], 'second_post_thumbnail', true); 13 | 14 | if($image_id) { 15 | $post_data['secondPostThumbnail'] = apply_filters('rez_serialize_attachment', get_post($image_id)); 16 | } 17 | return $post_data; 18 | } 19 | add_filter('rez_serialize_post', 'add_second_featured_image'); 20 | ``` 21 | 22 | ```php 23 | /** 24 | * Add a custom formatted date to all posts 25 | * 26 | * @param array $post_data The post currently being processed by Rest-Easy 27 | */ 28 | function custom_format_date($post_data){ 29 | $post = get_post($post_data['id']); 30 | $post_data['dateFormatted'] = get_the_date('F j Y', $post); 31 | return $post_data; 32 | } 33 | add_filter('rez_serialize_post', 'custom_format_date'); 34 | ``` 35 | 36 | ```php 37 | /** 38 | * Add custom credits to each post. Add line breaks while doing it. 39 | * 40 | * @param array $post_data The post currently being processed by Rest-Easy 41 | */ 42 | function custom_format_credits($post_data){ 43 | $post = get_post($post_data['id']); 44 | $post_data['meta']['customCredits'] = nl2br($post->custom_credits); 45 | return $post_data; 46 | } 47 | add_filter('rez_serialize_post', 'custom_format_credits'); 48 | ``` 49 | 50 | ```php 51 | /** 52 | * Overwrite the loop when in a tree to always be the children of the parent page. 53 | * This is useful when you want a child page to actually return all siblings as the main loop. 54 | * Common for contact pages that show as an accordian menu. 55 | * 56 | * @param array $loop All the pages/posts in the cuurent loop 57 | */ 58 | function serialize_contact_pages($loop){ 59 | 60 | $page = get_page_by_dev_id('contact'); 61 | 62 | if( is_tree($page->ID) ) { 63 | $loop = array(); 64 | $loop[0] = apply_filters('rez_serialize_object', get_post($page->ID)); 65 | $loop[0]['related'] = apply_filters('rez_gather_related', get_post($page->ID)); 66 | } 67 | return $loop; 68 | } 69 | add_filter('rez_build_loop_data', 'serialize_contact_pages'); 70 | ``` 71 | 72 | ## Example of some useful shortcodes 73 | 74 | Here are a bunch of useful examples of how to use WordPress shortcodes with vuehaus. You will need both the PHP and the Vue template. 75 | 76 | ```php 77 | /* 78 | * Image component (left or right aligned), with text on either side of it. 79 | */ 80 | function custom_shortcode_image( $atts, $content = '', $name) { 81 | $props = ''; 82 | $content = custom_filter_shortcode_text($content); 83 | 84 | // Include default props here 85 | extract(shortcode_atts(array( 86 | 'id' => '' // ID of WordPress attachment/image 87 | ), $atts)); 88 | 89 | if( !empty($id) ) { 90 | $image = apply_filters('rez_serialize_attachment', get_post($id)); 91 | 92 | // Props to pass to Vue component 93 | $props = ':image="' . esc_attr(json_encode($image)) . '"'; 94 | } 95 | 96 | return ''. $content .''; 97 | } 98 | add_shortcode( 'image-left', 'custom_shortcode_image' ); 99 | add_shortcode( 'image-right', 'custom_shortcode_image' ); 100 | ``` 101 | 102 | ```vue 103 | 115 | 116 | 135 | 136 | 172 | ``` 173 | 174 | ```php 175 | /* 176 | * Half column of text 177 | */ 178 | function custom_shortcode_half_column( $atts, $content = '', $name ) { 179 | $content = custom_filter_shortcode_text($content); 180 | return ''. $content .''; 181 | } 182 | add_shortcode( 'half-column', 'custom_shortcode_half_column' ); 183 | ``` 184 | 185 | ```vue 186 | 193 | 194 | 197 | 198 | 210 | ``` 211 | 212 | ```php 213 | /** 214 | * Add serialized version of blog page to site data 215 | */ 216 | function add_blog_permalink_to_sitedata($site_data){ 217 | $blog_page = get_page_by_dev_id('blog'); 218 | if ( $blog_page ) { 219 | $site_data['blogPage'] = apply_filters('rez_serialize_post', $blog_page); 220 | } 221 | return $site_data; 222 | } 223 | add_filter('rez_build_site_data', 'add_blog_permalink_to_sitedata'); 224 | ``` 225 | -------------------------------------------------------------------------------- /functions/wp-functions.php: -------------------------------------------------------------------------------- 1 | ID) ) { 118 | $content = '

'.get_the_post_thumbnail($post->ID).'

'.$content; 119 | } 120 | 121 | return $content; 122 | } 123 | add_filter('the_excerpt_rss', 'rss_post_thumbnail'); 124 | 125 | 126 | /* 127 | * Custom conditional function. Used to get the parent and all it's child. 128 | */ 129 | function is_tree($tree_id, $target_post = null) { 130 | // get full post object 131 | $target_post = get_post($target_post); 132 | // get all post ancestors 133 | $ancestors = get_ancestors($target_post->ID, $target_post->post_type); 134 | // if ID is target post OR in target post tree, return true 135 | return ( ($target_post->ID == $tree_id) or in_array($tree_id, $ancestors) ); 136 | } 137 | 138 | 139 | /* 140 | * Allow SVG uploads 141 | */ 142 | function add_mime_types($mimes) { 143 | $mimes['svg'] = 'image/svg+xml'; 144 | return $mimes; 145 | } 146 | //add_filter('upload_mimes', 'add_mime_types'); 147 | 148 | 149 | /* 150 | * Allow subscriber to see Private posts/pages 151 | */ 152 | function add_theme_caps() { 153 | // Gets the author role 154 | $role = get_role('subscriber'); 155 | 156 | // Add capabilities 157 | $role->add_cap('read_private_posts'); 158 | $role->add_cap('read_private_pages'); 159 | } 160 | //add_action( 'switch_theme', 'add_theme_caps'); 161 | 162 | 163 | /* 164 | * Change the [...] that comes after excerpts 165 | */ 166 | function custom_excerpt_ellipsis( $more ) { 167 | return '...'; 168 | } 169 | add_filter('excerpt_more', 'custom_excerpt_ellipsis'); 170 | 171 | 172 | /* 173 | * Add Google Analytics tracking settings to admin dashboard 174 | */ 175 | function my_general_section() { 176 | add_settings_section( 177 | 'vh_google_analytics_section', // Section ID 178 | 'Google Analytics Tracking IDs', // Section Title 179 | 'vh_google_analytics_section', // Callback 180 | 'general' // This makes the section show up on the General Settings Page 181 | ); 182 | 183 | add_settings_field( 184 | 'ga_tracking_code_1', // Option ID 185 | 'Tracking ID #1', // Label 186 | 'vh_google_analytics_settings', // !important - This is where the args go! 187 | 'general', // Page it will be displayed (General Settings) 188 | 'vh_google_analytics_section', // Name of our section 189 | array( 190 | 'ga_tracking_code_1' // Should match Option ID 191 | ) 192 | ); 193 | 194 | add_settings_field( 195 | 'ga_tracking_code_2', // Option ID 196 | 'Tracking ID #2', // Label 197 | 'vh_google_analytics_settings', // !important - This is where the args go! 198 | 'general', // Page it will be displayed (General Settings) 199 | 'vh_google_analytics_section', // Name of our section 200 | array( 201 | 'ga_tracking_code_2' // Should match Option ID 202 | ) 203 | ); 204 | 205 | register_setting('general','ga_tracking_code_1', 'esc_attr'); 206 | register_setting('general','ga_tracking_code_2', 'esc_attr'); 207 | } 208 | add_action('admin_init', 'my_general_section'); 209 | 210 | 211 | /* 212 | * Settings callbacks that build the Analytics markup 213 | */ 214 | function vh_google_analytics_section() { 215 | echo '

Enter Google Analytics tracking codes. Uses the gtag.js tracking method.

'; 216 | } 217 | 218 | function vh_google_analytics_settings($args) { 219 | $option = get_option($args[0]); 220 | echo ''; 221 | } 222 | 223 | /* 224 | * Add theme options page 225 | */ 226 | // if( function_exists('acf_add_options_page') ) { 227 | // acf_add_options_page(array( 228 | // 'page_title' => 'Theme Settings', 229 | // 'menu_title' => 'Theme Settings', 230 | // 'menu_slug' => 'theme-settings' 231 | // )); 232 | // } 233 | -------------------------------------------------------------------------------- /functions/rest-easy-filters.php: -------------------------------------------------------------------------------- 1 | $value ){ 50 | $post_data['acf'][$name] = $value; 51 | } 52 | 53 | } 54 | 55 | return $post_data; 56 | } 57 | add_filter('rez_serialize_post', 'serialize_custom_acf_content'); 58 | 59 | /* 60 | * Add Focushaus focal point and Funky-Colors colors to ACF images 61 | */ 62 | function add_focal_point_to_acf_images($target){ 63 | 64 | if( function_exists('get_offset_x') ){ 65 | $target['focus'] = array( 66 | 'x' => get_offset_x( $target['ID'] ), 67 | 'y' => get_offset_y( $target['ID'] ) 68 | ); 69 | } 70 | 71 | if( function_exists('get_primary_image_color') ){ 72 | $target['primary_color'] = get_primary_image_color($target['ID']); 73 | $target['secondary_color'] = get_second_image_color($target['ID']); 74 | } 75 | 76 | $image['videoSrc'] = $target->custom_video_url; 77 | 78 | return $target; 79 | } 80 | add_filter('acf/format_value/type=image', 'add_focal_point_to_acf_images', 20, 4); 81 | 82 | /* 83 | * Add Focushaus focal point and Funky-Colors colors to ACF gallery images 84 | */ 85 | function add_image_data_to_acf_galleries($value){ 86 | 87 | foreach ($value as $index => &$image) { 88 | $target_post = get_post($image['id']); 89 | 90 | if( function_exists('get_offset_x') ){ 91 | $image['focus'] = array( 92 | 'x' => get_offset_x( $image['id'] ), 93 | 'y' => get_offset_y( $image['id'] ) 94 | ); 95 | } 96 | if( function_exists('get_primary_image_color') ){ 97 | $image['primary_color'] = get_primary_image_color($image['id']); 98 | $image['secondary_color'] = get_second_image_color($image['id']); 99 | } 100 | 101 | // add video to gallery image 102 | $image['meta']['custom_video_url'] = $target_post->custom_video_url; 103 | 104 | } 105 | return $value; 106 | } 107 | add_filter('acf/format_value/type=gallery', 'add_image_data_to_acf_galleries', 20, 4); 108 | 109 | /** 110 | * Add pages with dev ID to site data 111 | * 112 | * @param array $input The nav item currently being processed by Rest-Easy 113 | */ 114 | function add_dev_page_links_to_site_data($site_data){ 115 | 116 | $args = array( 117 | 'posts_per_page' => -1, 118 | 'orderby' => 'menu_order', 119 | 'order' => 'ASC', 120 | // get all non-empty custom_developer_id pages 121 | 'meta_query' => array( 122 | array( 123 | 'key' => 'custom_developer_id', 124 | 'value' => array(''), 125 | 'compare' => 'NOT IN' 126 | ) 127 | ), 128 | 'post_type' => 'page', 129 | ); 130 | $posts_array = get_posts( $args ); 131 | 132 | $site_data['devIds'] = array(); 133 | 134 | foreach($posts_array as $dev_id_page){ 135 | $key = $dev_id_page->custom_developer_id; 136 | $site_data['devIds'][$key] = wp_make_link_relative(get_permalink($dev_id_page)); 137 | } 138 | 139 | return $site_data; 140 | } 141 | add_filter('rez_build_site_data', 'add_dev_page_links_to_site_data'); 142 | 143 | /** 144 | * Serialize page siblings 145 | * 146 | * @param array $input The post currently being processed by Rest-Easy 147 | */ 148 | 149 | // NOTE: This is a common Rest-Easy filter, but including it by default can slow 150 | // down load times on large sites. Uncomment to use, but it's best to 151 | // include some extra checks so this doesn't run on every page. 152 | 153 | // function add_page_siblings($related, $target = null){ 154 | // $target = get_post($target); 155 | // $args = array( 156 | // 'posts_per_page' => -1, 157 | // 'orderby' => 'menu_order', 158 | // 'order' => 'ASC', 159 | // 'exclude' => array( $target->ID ), 160 | // 'post_type' => 'page', 161 | // 'post_parent' => $target->post_parent 162 | // ); 163 | // $siblings = get_posts($args); 164 | // 165 | // // apply the serialize_object filter to all siblings 166 | // $related['siblings'] = array_map( 167 | // function ($sibling) { return apply_filters('rez_serialize_object', $sibling); }, 168 | // $siblings 169 | // ); 170 | // 171 | // // return modified data 172 | // return $related; 173 | // } 174 | // add_filter('rez_gather_related', 'add_page_siblings', 10, 2); 175 | 176 | // Add theme options to site data 177 | // function add_theme_options_to_site($site_data){ 178 | // if ( function_exists('the_field') ){ 179 | // $site_data['themeOptions'] = array( 180 | // 'singleText' => get_field('single_text', 'option'), 181 | // 'multilineText' => explode("\n", get_field('multiline_text', 'option')), 182 | // ); 183 | // } 184 | // return $site_data; 185 | // } 186 | // add_filter('rez_build_site_data', 'add_theme_options_to_site'); 187 | 188 | // Adds adjacent posts when they are unset due to recent bug in wordpress. 189 | //function add_adjacent_posts($related, $post = null) { 190 | // if (is_null($related['next']) && is_null($related['prev'])) { 191 | // $prev = get_posts(array( 192 | // 'post_type' => 'post', 193 | // 'posts_per_page' => 1, 194 | // 'date_query' => array( 195 | // 'after' => $post->post_date 196 | // ) 197 | // )); 198 | // $next = get_posts(array( 199 | // 'post_type' => 'post', 200 | // 'posts_per_page' => 1, 201 | // 'date_query' => array( 202 | // 'before' => $post->post_date 203 | // ) 204 | // )); 205 | // if (count($next) > 0){ 206 | // $related['next'] = apply_filters('rez_serialize_object', $next[0]); 207 | // } 208 | // if (count($prev) > 0){ 209 | // $related['prev'] = apply_filters('rez_serialize_object', $prev[0]); 210 | // } 211 | // } 212 | // return $related; 213 | // } 214 | // add_filter('rez_gather_related', 'add_adjacent_posts', 10, 2); 215 | 216 | /** 217 | * Add WP-Shopify options to site data 218 | * 219 | * @param array $site_data The site data currently being processed by Rest-Easy 220 | */ 221 | function add_wps_options_to_site_data($site_data){ 222 | if ( function_exists('get_wshop_domain') ){ 223 | $site_data['storefrontToken'] = get_wshop_api_key(); 224 | $site_data['shopifyDomain'] = get_wshop_domain(); 225 | } 226 | return $site_data; 227 | } 228 | add_filter('rez_build_site_data', 'add_wps_options_to_site_data'); 229 | 230 | /** 231 | * Add next posts link to site data for infinite scroll 232 | * 233 | * @param array $site_data The site data currently being processed by Rest-Easy 234 | */ 235 | function add_next_post_page_link($input) { 236 | $input['meta']['next_post_link'] = next_posts(0, false); 237 | return $input; 238 | } 239 | add_filter('rez_build_site_data', 'add_next_post_page_link'); 240 | -------------------------------------------------------------------------------- /functions/developer-role.php: -------------------------------------------------------------------------------- 1 | get_role('administrator'); 11 | 12 | add_role( 13 | 'developer', 14 | __('Developer'), 15 | $admin_role->capabilities 16 | ); 17 | 18 | // set initial user to Developer 19 | $user = new WP_User(1); 20 | $user->set_role('developer'); 21 | } 22 | add_action('after_switch_theme', 'custom_theme_switch'); 23 | 24 | /* 25 | * Disable Rich Editor on certain pages 26 | */ 27 | function disabled_rich_editor($allow_rich_editor) { 28 | global $post; 29 | 30 | if($post->_custom_hide_richedit === 'on') { 31 | return false; 32 | } 33 | return true; 34 | } 35 | add_filter( 'user_can_richedit', 'disabled_rich_editor'); 36 | 37 | /* 38 | * Deactivate attachment serialization on certain pages 39 | */ 40 | function deactivate_attachment_serialization($allow_attachment_serialization) { 41 | global $post; 42 | 43 | if($post->_custom_deactivate_attachment_serialization === 'on') { 44 | return false; 45 | } 46 | return true; 47 | } 48 | add_filter( 'rez-serialize-attachments', 'deactivate_attachment_serialization'); 49 | 50 | /* 51 | * Add developer metaboxes to the new/edit page 52 | */ 53 | function custom_add_developer_metaboxes($post_type, $post){ 54 | if( !user_is_developer() ) return; 55 | 56 | add_meta_box('custom_dev_meta', 'Developer Meta', 'custom_dev_meta', 'page', 'normal', 'low'); 57 | } 58 | add_action('add_meta_boxes', 'custom_add_developer_metaboxes', 10, 2); 59 | 60 | 61 | // Build dev meta box (only for users with Developer role) 62 | function custom_dev_meta($post) { 63 | 64 | ?> 65 |
66 | 67 | 68 |
69 | 70 | 71 | _custom_lock ) echo 'checked'; ?>> 72 |
73 | 74 | 75 | _custom_hide_richedit === 'on' ) echo 'checked'; ?>> 76 |
77 | 78 | 79 | _custom_deactivate_attachment_serialization === 'on' ) echo 'checked'; ?>> 80 |
81 | 82 |
83 | 84 | _custom_lock ){ 92 | echo 'Only a user with the Developer role can delete this page.

'; 93 | echo 'Back'; 94 | exit; 95 | } 96 | } 97 | add_action('wp_trash_post', 'check_custom_post_lock', 10, 1); 98 | add_action('before_delete_post', 'check_custom_post_lock', 10, 1); 99 | 100 | function custom_save_developer_metabox($post_id){ 101 | 102 | // check autosave 103 | if( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) { 104 | return $post_id; 105 | } 106 | 107 | // these values will only be updated if the current user is a Developer 108 | if( !user_is_developer() ) return; 109 | 110 | if( isset($_POST['custom_developer_id']) ) { 111 | update_post_meta($post_id, 'custom_developer_id', $_POST['custom_developer_id']); 112 | } 113 | if( isset($_POST['_custom_lock']) ) { 114 | $value = $_POST['_custom_lock'] == 'on' ? 'on' : 0; 115 | update_post_meta($post_id, '_custom_lock', $value); 116 | } else { 117 | update_post_meta($post_id, '_custom_lock', 0); 118 | } 119 | 120 | if( isset($_POST['_custom_hide_richedit']) ){ 121 | $value = $_POST['_custom_hide_richedit'] == 'on' ? 'on' : 0; 122 | update_post_meta($post_id, '_custom_hide_richedit', $_POST['_custom_hide_richedit']); 123 | } else { 124 | update_post_meta($post_id, '_custom_hide_richedit', 0); 125 | } 126 | 127 | if( isset($_POST['_custom_deactivate_attachment_serialization']) ){ 128 | $value = $_POST['_custom_deactivate_attachment_serialization'] == 'on' ? 'on' : 0; 129 | update_post_meta($post_id, '_custom_deactivate_attachment_serialization', $_POST['_custom_deactivate_attachment_serialization']); 130 | } else { 131 | update_post_meta($post_id, '_custom_deactivate_attachment_serialization', 0); 132 | } 133 | 134 | } 135 | add_action('save_post', 'custom_save_developer_metabox'); 136 | 137 | function user_is_developer(){ 138 | $roles = wp_get_current_user()->roles; 139 | return in_array( 'developer', $roles ); 140 | } 141 | 142 | // Gets page by a given dev ID 143 | function get_page_by_dev_id( $dev_id ){ 144 | $args = array( 145 | 'posts_per_page' => 1, 146 | 'meta_key' => 'custom_developer_id', 147 | 'meta_value' => $dev_id, 148 | 'post_type' => 'page', 149 | ); 150 | return reset( get_posts($args) ); 151 | } 152 | 153 | // Convenience function - get relative path by dev ID 154 | function path_from_dev_id($dev_id, $after = ''){ 155 | $retrieved_page = get_page_by_dev_id($dev_id); 156 | 157 | if( !$retrieved_page ){ 158 | return '#404'; 159 | } 160 | 161 | $base_url = rtrim(wp_make_link_relative(get_permalink($retrieved_page)), '/'); 162 | return $base_url . $after; 163 | } 164 | 165 | // Convenience function - get slug by dev ID 166 | function slug_from_dev_id($dev_id){ 167 | $retrieved_page = get_page_by_dev_id($dev_id); 168 | 169 | if( !$retrieved_page ){ 170 | return ''; 171 | } 172 | 173 | return $retrieved_page->post_name; 174 | } 175 | 176 | // Gets the nth child of a page with a given Developer ID 177 | function get_child_of_dev_id($dev_id, $nth_child = 0){ 178 | $parent = get_page_by_dev_id($dev_id); 179 | 180 | $args = array( 181 | 'posts_per_page' => 1, 182 | 'offset' => $nth_child, 183 | 'orderby' => 'menu_order', 184 | 'order' => 'ASC', 185 | 'post_type' => 'page', 186 | 'post_parent' => $parent->ID, 187 | ); 188 | return reset(get_posts($args)); 189 | } 190 | 191 | // Gets the relative path of the nth child of a page with given Developer ID 192 | function get_child_of_dev_id_path($dev_id, $nth_child = 0, $after = ''){ 193 | $permalink = get_permalink(get_child_of_dev_id($dev_id, $nth_child)); 194 | return $base_url = rtrim($permalink, '/'); 195 | } 196 | 197 | // Gets the children of a page with the given Developer ID 198 | function get_children_of_dev_id($dev_id){ 199 | $parent = get_page_by_dev_id($dev_id); 200 | 201 | if( $parent === false ) { 202 | return false; 203 | } 204 | 205 | $args = array( 206 | 'posts_per_page' => -1, 207 | 'post_type' => 'page', 208 | 'orderby' => 'menu_order', 209 | 'order' => 'ASC', 210 | 'post_parent' => $parent->ID, 211 | ); 212 | return get_posts($args); 213 | } 214 | 215 | // Makes sure Developer role can sort Nested Pages automatically 216 | function give_developer_ordering_permissions(){ 217 | 218 | if( is_plugin_active('wp-nested-pages/nestedpages.php') ){ 219 | 220 | $allowed_to_sort = get_option('nestedpages_allowsorting'); 221 | 222 | if( !$allowed_to_sort ){ 223 | $allowed_to_sort = array(); 224 | } 225 | 226 | if( !in_array('developer', $allowed_to_sort) ){ 227 | $allowed_to_sort[] = 'developer'; 228 | update_option('nestedpages_allowsorting', $allowed_to_sort); 229 | } 230 | } 231 | 232 | } 233 | add_action('admin_init', 'give_developer_ordering_permissions', 1); 234 | 235 | // add 'is-developer' class to WP admin pages if we're a developer 236 | function add_developer_admin_body_class($classes){ 237 | if( user_is_developer() ){ 238 | $classes .= ' is-developer'; 239 | } 240 | return $classes; 241 | } 242 | add_filter('admin_body_class', 'add_developer_admin_body_class'); 243 | 244 | // add extra capabilities for common plugins 245 | function add_extra_vh_capabilities(){ 246 | global $wp_roles; 247 | 248 | // add desired developer capabilities here 249 | $caps_to_add = array( 250 | 'manage_woocommerce', 251 | 'view_woocommerce_reports' 252 | ); 253 | 254 | // woocommerce defaults 255 | // (https://github.com/woocommerce/woocommerce/blob/master/includes/class-wc-install.php#L981) 256 | $capability_types = array( 'product', 'shop_order', 'shop_coupon' ); 257 | foreach ( $capability_types as $capability_type ) { 258 | $wc_caps_to_add = array( 259 | // Post type. 260 | "edit_{$capability_type}", 261 | "read_{$capability_type}", 262 | "delete_{$capability_type}", 263 | "edit_{$capability_type}s", 264 | "edit_others_{$capability_type}s", 265 | "publish_{$capability_type}s", 266 | "read_private_{$capability_type}s", 267 | "delete_{$capability_type}s", 268 | "delete_private_{$capability_type}s", 269 | "delete_published_{$capability_type}s", 270 | "delete_others_{$capability_type}s", 271 | "edit_private_{$capability_type}s", 272 | "edit_published_{$capability_type}s", 273 | // Terms. 274 | "manage_{$capability_type}_terms", 275 | "edit_{$capability_type}_terms", 276 | "delete_{$capability_type}_terms", 277 | "assign_{$capability_type}_terms", 278 | ); 279 | 280 | foreach($wc_caps_to_add as $wc_cap_to_add){ 281 | $caps_to_add[] = $wc_cap_to_add; 282 | } 283 | } 284 | 285 | // get developer role 286 | $developer_role = get_role('developer'); 287 | 288 | // add desired caps to developer if they don't exist yet 289 | foreach($caps_to_add as $cap_to_add){ 290 | if( !$developer_role->has_cap($cap_to_add) ){ 291 | $wp_roles->add_cap( 'developer', $cap_to_add ); 292 | } 293 | } 294 | } 295 | // add permissions after any plugin activated 296 | add_action('activated_plugin', 'add_extra_vh_capabilities'); 297 | -------------------------------------------------------------------------------- /blocks/dist/blocks.js: -------------------------------------------------------------------------------- 1 | /******/ ;(function(modules) { 2 | // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {} // The require function 5 | /******/ 6 | /******/ /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if (installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports 11 | /******/ 12 | } // Create a new module (and put it into the cache) 13 | /******/ /******/ var module = (installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ 18 | }) // Execute the module function 19 | /******/ 20 | /******/ /******/ modules[moduleId].call( 21 | module.exports, 22 | module, 23 | module.exports, 24 | __webpack_require__ 25 | ) // Flag the module as loaded 26 | /******/ 27 | /******/ /******/ module.l = true // Return the exports of the module 28 | /******/ 29 | /******/ /******/ return module.exports 30 | /******/ 31 | } // expose the modules object (__webpack_modules__) 32 | /******/ 33 | /******/ 34 | /******/ /******/ __webpack_require__.m = modules // expose the module cache 35 | /******/ 36 | /******/ /******/ __webpack_require__.c = installedModules // define getter function for harmony exports 37 | /******/ 38 | /******/ /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if (!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { 41 | /******/ configurable: false, 42 | /******/ enumerable: true, 43 | /******/ get: getter 44 | /******/ 45 | }) 46 | /******/ 47 | } 48 | /******/ 49 | } // getDefaultExport function for compatibility with non-harmony modules 50 | /******/ 51 | /******/ /******/ __webpack_require__.n = function(module) { 52 | /******/ var getter = 53 | module && module.__esModule 54 | ? /******/ function getDefault() { 55 | return module['default'] 56 | } 57 | : /******/ function getModuleExports() { 58 | return module 59 | } 60 | /******/ __webpack_require__.d(getter, 'a', getter) 61 | /******/ return getter 62 | /******/ 63 | } // Object.prototype.hasOwnProperty.call 64 | /******/ 65 | /******/ /******/ __webpack_require__.o = function(object, property) { 66 | return Object.prototype.hasOwnProperty.call(object, property) 67 | } // __webpack_public_path__ 68 | /******/ 69 | /******/ /******/ __webpack_require__.p = '' // Load entry module and return exports 70 | /******/ 71 | /******/ /******/ return __webpack_require__((__webpack_require__.s = 0)) 72 | /******/ 73 | })( 74 | /************************************************************************/ 75 | /******/ [ 76 | /* 0 */ 77 | /***/ function(module, exports, __webpack_require__) { 78 | __webpack_require__(1) 79 | module.exports = __webpack_require__(6) 80 | 81 | /***/ 82 | }, 83 | /* 1 */ 84 | /***/ function(module, __webpack_exports__, __webpack_require__) { 85 | 'use strict' 86 | Object.defineProperty(__webpack_exports__, '__esModule', { 87 | value: true 88 | }) 89 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__utils_builder__ = __webpack_require__( 90 | 2 91 | ) 92 | 93 | // Example block 94 | // buildBlock({ 95 | // name: 'Title and Image', 96 | // slug: 'title-and-image', 97 | // description: 'A title and an image.', 98 | // class: 'title-and-image', 99 | // content: [ 100 | //

Title

, 101 | // { name: 'content', type: 'text' }, 102 | //

Image

, 103 | // { name: 'image', type: 'image' } 104 | // ] 105 | // }) 106 | 107 | /***/ 108 | }, 109 | /* 2 */ 110 | /***/ function(module, __webpack_exports__, __webpack_require__) { 111 | 'use strict' 112 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__prebuilt__ = __webpack_require__( 113 | 3 114 | ) 115 | var _extends = 116 | Object.assign || 117 | function(target) { 118 | for (var i = 1; i < arguments.length; i++) { 119 | var source = arguments[i] 120 | for (var key in source) { 121 | if ( 122 | Object.prototype.hasOwnProperty.call( 123 | source, 124 | key 125 | ) 126 | ) { 127 | target[key] = source[key] 128 | } 129 | } 130 | } 131 | return target 132 | } 133 | 134 | // prep prebuilt components 135 | 136 | var registerBlockType = wp.blocks.registerBlockType 137 | var RichText = wp.editor.RichText 138 | 139 | /* unused harmony default export */ var _unused_webpack_default_export = function() { 140 | var opts = 141 | arguments.length > 0 && arguments[0] !== undefined 142 | ? arguments[0] 143 | : {} 144 | 145 | var settings = _extends( 146 | { 147 | // defaults 148 | slug: 'block-slug', 149 | name: 'Example', 150 | description: '', 151 | icon: 'format-gallery', 152 | content: [], 153 | class: 'block-class' 154 | }, 155 | opts 156 | ) 157 | 158 | // build attributes according to content children 159 | var attributes = {} 160 | settings.content.map(function(child) { 161 | if ( 162 | child.type && 163 | __WEBPACK_IMPORTED_MODULE_0__prebuilt__[ 164 | 'a' /* default */ 165 | ][child.type] 166 | ) { 167 | attributes[ 168 | child.name 169 | ] = __WEBPACK_IMPORTED_MODULE_0__prebuilt__[ 170 | 'a' /* default */ 171 | ][child.type].attributes(child) 172 | } 173 | }) 174 | 175 | // register desired block 176 | registerBlockType('custom-fh/' + settings.slug, { 177 | title: settings.name, 178 | description: settings.description, 179 | icon: settings.icon, 180 | category: 'custom-fh', 181 | 182 | // use attributes defined above 183 | attributes: attributes, 184 | 185 | // Editor 186 | edit: function edit(props) { 187 | var attributes = props.attributes, 188 | className = props.className, 189 | setAttributes = props.setAttributes 190 | 191 | var classes = settings.class + ' fh-custom-block' 192 | 193 | var output = settings.content 194 | .map(function(child) { 195 | if ( 196 | child.type && 197 | __WEBPACK_IMPORTED_MODULE_0__prebuilt__[ 198 | 'a' /* default */ 199 | ][child.type] 200 | ) { 201 | return __WEBPACK_IMPORTED_MODULE_0__prebuilt__[ 202 | 'a' /* default */ 203 | ][child.type].edit(props, child) 204 | } 205 | 206 | return child 207 | }) 208 | .filter(function(x) { 209 | return x 210 | }) 211 | 212 | return wp.element.createElement( 213 | 'div', 214 | { className: classes }, 215 | output 216 | ) 217 | }, 218 | 219 | // On save 220 | save: function save(props) { 221 | var output = settings.content 222 | .map(function(child) { 223 | if ( 224 | child.type && 225 | __WEBPACK_IMPORTED_MODULE_0__prebuilt__[ 226 | 'a' /* default */ 227 | ][child.type] 228 | ) { 229 | return __WEBPACK_IMPORTED_MODULE_0__prebuilt__[ 230 | 'a' /* default */ 231 | ][child.type].save(props, child) 232 | } 233 | 234 | return null 235 | }) 236 | .filter(function(x) { 237 | return x 238 | }) 239 | 240 | return wp.element.createElement( 241 | 'div', 242 | { className: settings.class }, 243 | output 244 | ) 245 | } 246 | }) 247 | } 248 | 249 | /***/ 250 | }, 251 | /* 3 */ 252 | /***/ function(module, __webpack_exports__, __webpack_require__) { 253 | 'use strict' 254 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__text__ = __webpack_require__( 255 | 4 256 | ) 257 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__image__ = __webpack_require__( 258 | 5 259 | ) 260 | 261 | /* harmony default export */ __webpack_exports__['a'] = { 262 | text: __WEBPACK_IMPORTED_MODULE_0__text__['a' /* default */], 263 | image: __WEBPACK_IMPORTED_MODULE_1__image__['a' /* default */] 264 | } 265 | 266 | /***/ 267 | }, 268 | /* 4 */ 269 | /***/ function(module, __webpack_exports__, __webpack_require__) { 270 | 'use strict' 271 | function _defineProperty(obj, key, value) { 272 | if (key in obj) { 273 | Object.defineProperty(obj, key, { 274 | value: value, 275 | enumerable: true, 276 | configurable: true, 277 | writable: true 278 | }) 279 | } else { 280 | obj[key] = value 281 | } 282 | return obj 283 | } 284 | 285 | var RichText = wp.editor.RichText 286 | 287 | /* harmony default export */ __webpack_exports__['a'] = { 288 | attributes: function attributes(child) { 289 | return { 290 | type: 'array', 291 | source: 'children', 292 | selector: '.' + child.name 293 | } 294 | }, 295 | edit: function edit(props, child) { 296 | var attributes = props.attributes, 297 | className = props.className, 298 | setAttributes = props.setAttributes 299 | 300 | var content = attributes[child.name] 301 | 302 | var classes = child.name + ' ' + className 303 | 304 | function onChangeContent(newContent) { 305 | setAttributes( 306 | _defineProperty({}, child.name, newContent) 307 | ) 308 | } 309 | 310 | return wp.element.createElement(RichText, { 311 | tagName: 'p', 312 | className: classes, 313 | onChange: onChangeContent, 314 | value: content 315 | }) 316 | }, 317 | save: function save(props, child) { 318 | // data attributes passed from editor 319 | var attributes = props.attributes 320 | 321 | // get content and classes relevant to this particular block 322 | 323 | var content = attributes[child.name] 324 | var classes = '' + child.name 325 | 326 | return wp.element.createElement(RichText.Content, { 327 | className: classes, 328 | tagName: 'p', 329 | value: content 330 | }) 331 | } 332 | } 333 | 334 | /***/ 335 | }, 336 | /* 5 */ 337 | /***/ function(module, __webpack_exports__, __webpack_require__) { 338 | 'use strict' 339 | function _defineProperty(obj, key, value) { 340 | if (key in obj) { 341 | Object.defineProperty(obj, key, { 342 | value: value, 343 | enumerable: true, 344 | configurable: true, 345 | writable: true 346 | }) 347 | } else { 348 | obj[key] = value 349 | } 350 | return obj 351 | } 352 | 353 | var MediaUpload = wp.editor.MediaUpload 354 | var Button = wp.components.Button 355 | 356 | /* harmony default export */ __webpack_exports__['a'] = { 357 | // attributes 358 | attributes: function attributes(child) { 359 | return { 360 | mediaID: { 361 | type: 'number' 362 | }, 363 | mediaURL: { 364 | type: 'string', 365 | source: 'attribute', 366 | selector: 'img', 367 | attribute: 'src' 368 | }, 369 | width: { 370 | type: 'number' 371 | }, 372 | height: { 373 | type: 'number' 374 | } 375 | } 376 | }, 377 | 378 | // editing 379 | edit: function edit(props, child) { 380 | var setAttributes = props.setAttributes, 381 | attributes = props.attributes 382 | 383 | // try to find correct attributes, default to empty object 384 | 385 | var atts = attributes[child.name] || {} 386 | var mediaID = atts.mediaID, 387 | mediaURL = atts.mediaURL 388 | 389 | // selecting the image 390 | 391 | function onSelectImage(newImage) { 392 | // console.log(newImage) 393 | setAttributes( 394 | _defineProperty({}, child.name, { 395 | mediaURL: newImage.url, 396 | mediaID: newImage.id, 397 | width: newImage.width, 398 | height: newImage.height 399 | }) 400 | ) 401 | } 402 | 403 | // edit block 404 | return wp.element.createElement(MediaUpload, { 405 | onSelect: onSelectImage, 406 | type: 'image', 407 | value: mediaID, 408 | render: function render(_ref) { 409 | var open = _ref.open 410 | return wp.element.createElement( 411 | Button, 412 | { 413 | className: mediaID 414 | ? 'image-button' 415 | : 'button button-large', 416 | onClick: open 417 | }, 418 | !mediaID 419 | ? 'Upload Image' 420 | : wp.element.createElement('img', { 421 | src: mediaURL 422 | }) 423 | ) 424 | } 425 | }) 426 | }, 427 | save: function save(props, child) { 428 | var attributes = props.attributes 429 | 430 | // ignore if we don't have the correct content 431 | 432 | if (!attributes || !attributes[child.name]) { 433 | return '' 434 | } 435 | 436 | var _attributes$child$nam = attributes[child.name], 437 | mediaID = _attributes$child$nam.mediaID, 438 | mediaURL = _attributes$child$nam.mediaURL, 439 | width = _attributes$child$nam.width, 440 | height = _attributes$child$nam.height 441 | 442 | if (mediaURL) { 443 | return wp.element.createElement('img', { 444 | 'data-replace-with': 'responsive-image', 445 | src: mediaURL, 446 | 'data-wp-id': mediaID, 447 | width: width, 448 | height: height 449 | }) 450 | } 451 | 452 | return '' 453 | } 454 | } 455 | 456 | /***/ 457 | }, 458 | /* 6 */ 459 | /***/ function(module, exports) { 460 | // removed by extract-text-webpack-plugin 461 | /***/ 462 | } 463 | /******/ 464 | ] 465 | ) 466 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **DEPRECATED: We have stopped developing this, check out our new boilerplate [stackhaus](https://github.com/funkhaus/stackhaus).** 2 | 3 | Vuehaus is a boilerplate used to build fast, responsive [WordPress](https://wordpress.org/) templates with [Vue.js](https://vuejs.org/). Built by [Funkhaus](http://funkhaus.us/). 4 | 5 | # Tutorial 6 | 7 | Head over to the [tutorial](https://github.com/funkhaus/vuehaus/wiki) to learn how to build Vue.js + WordPress sites with Vuehaus! 8 | 9 | # Reference 10 | 11 | ## Table of Contents 12 | 13 | 1. [Install](#install) 14 | 1. [Common Tasks](#common-tasks) 15 | 1. [Getting Information from The Loop](#getting-information-from-the-loop) 16 | 1. [Loading Fonts](#loading-fonts) 17 | 1. [Using SVGs](#using-svgs) 18 | 1. [Images](#images) 19 | 1. [Images with Videos](#images-with-videos) 20 | 1. [Shared Styles](#shared-styles) 21 | 1. [The Developer Role and Developer IDs](#the-developer-role-and-developer-ids) 22 | 1. [Developer Capabilities](#developer-capabilities) 23 | 1. [Advanced Routing](#advanced-routing) 24 | 1. [Front Page Children](#front-page-children) 25 | 1. [Utility Functions](#utility-functions) 26 | 1. [Upgrading Plugins](#upgrading-plugins) 27 | 1. [Vuex and State](#vuex-and-state) 28 | 1. [Mutations](#mutations) 29 | 1. [Actions](#actions) 30 | 1. [Getters](#getters) 31 | 1. [Vuehaus Events](#vuehaus-events) 32 | 1. [Partials](#partials) 33 | 1. [Gutenberg Blocks](#gutenberg-blocks) 34 | 1. [Adding a Block](#adding-a-block) 35 | 1. [Block Content](#block-content) 36 | 1. [Block Content Types](#block-content-types) 37 | 1. [Adding Content Types](#adding-content-types) 38 | 1. [Rendering Gutenberg Content](#rendering-gutenberg-content) 39 | 1. [Removing Block Support](#removing-block-support) 40 | 1. [Deploying](#deploying) 41 | 1. [Recommended Reading](#recommended-reading) 42 | 1. [Contributing](#contributing) 43 | 44 | ## Install 45 | 46 | 1. In a WordPress `themes/` directory: `git clone https://github.com/funkhaus/vuehaus my-theme` 47 | 1. `cd my-theme` 48 | 1. `npm install` 49 | 1. Go to the WordPress back-end, activate, the Vuehaus theme, and follow the instructions to install [Rest-Easy](https://github.com/funkhaus/Rest-Easy). 50 | 1. `npm run dev` 51 | 1. To build: `npm run build` 52 | 1. (Optional) To build and deploy to server using [fh-deploy](https://github.com/funkhaus/fh-deploy): `npm run deploy` 53 | 54 | ## Common Tasks 55 | 56 | ### Getting Information from The Loop 57 | 58 | Paste these anywhere in your `