├── resources ├── screenshots │ ├── recipe01.png │ └── recipe02.png ├── css │ └── fields │ │ └── RecipeFieldType.css ├── js │ └── fields │ │ └── RecipeFieldType.js ├── star.svg ├── icon-mask.svg ├── icon.svg └── healthy.svg ├── composer.json ├── translations └── en.php ├── templates ├── fields │ ├── RecipeFieldType_Settings.twig │ └── RecipeFieldType.twig └── welcome.twig ├── LICENSE.txt ├── releases.json ├── RecipePlugin.php ├── fieldtypes └── RecipeFieldType.php ├── README.md └── models └── RecipeModel.php /resources/screenshots/recipe01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/recipe/master/resources/screenshots/recipe01.png -------------------------------------------------------------------------------- /resources/screenshots/recipe02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/recipe/master/resources/screenshots/recipe02.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/recipe", 3 | "description": "A comprehensive recipe FieldType for Craft CMS that includes metric/imperial conversion, portion calculation, and JSON-LD microdata support", 4 | "require": { 5 | "composer/installers": "~1.0" 6 | }, 7 | "type": "craft-plugin" 8 | } -------------------------------------------------------------------------------- /translations/en.php: -------------------------------------------------------------------------------- 1 | 'To this', 16 | ); 17 | -------------------------------------------------------------------------------- /templates/fields/RecipeFieldType_Settings.twig: -------------------------------------------------------------------------------- 1 | {% import "_includes/forms" as forms %} 2 | 3 |
4 | {% if assetSources %} 5 | {{ forms.checkboxSelectField({ 6 | label: "Asset Sources"|t, 7 | instructions: "Which sources do you want to select assets from?"|t, 8 | id: 'assetSources', 9 | name: 'assetSources', 10 | options: assetSources, 11 | values: settings.assetSources 12 | })}} 13 | {% else %} 14 | {{ forms.field({ 15 | label: "Asset Sources"|t, 16 | }, '

' ~ "No asset sources exist yet."|t ~ '

') }} 17 | {% endif %} 18 | 19 |
-------------------------------------------------------------------------------- /resources/css/fields/RecipeFieldType.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Recipe plugin for Craft CMS 3 | * 4 | * RecipeFieldType CSS 5 | * 6 | * @author nystudio107 7 | * @copyright Copyright (c) 2016 nystudio107 8 | * @link http://nystudio107.com 9 | * @package Recipe 10 | * @since 1.0.0 11 | */ 12 | 13 | div.recipe-field { 14 | border: 1px solid #e3e5e8; 15 | margin-bottom: 10px; 16 | padding: 14px 14px 14px; 17 | border-radius: 3px; 18 | background: #f9fafa; 19 | } 20 | 21 | div.recipe-field-title { 22 | background: #eef0f1; 23 | color: #8f98a3; 24 | margin: -7px -14px 14px; 25 | padding: 7px 14px 7px 14px; 26 | width: calc(100% + 28px); 27 | -webkit-box-sizing: border-box; 28 | -moz-box-sizing: border-box; 29 | box-sizing: border-box; 30 | border-radius: 2px 2px 0 0; 31 | overflow: hidden; 32 | white-space: nowrap; 33 | text-overflow: ellipsis; 34 | word-wrap: normal; 35 | cursor: default; 36 | } 37 | 38 | img.recipe-field-icon { 39 | display: inline-block; 40 | width: 16px; 41 | height: auto; 42 | padding-right: 7px; 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The Recipe License 2 | Copyright (c) 2016 nystudio107 3 | 4 | Permission is hereby granted, free of charge, to any person or entity obtaining a copy of this software and associated documentation files (the "Software"), to use the software in any capacity, including commercial and for-profit use. Permission is also granted to alter, modify, or extend the Software for your own use, or commission a third-party to perform modifications for you. 5 | 6 | Permission is NOT granted to create derivative works, sublicense, and/or sell copies of the Software. This is not FOSS software. 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /releases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "1.0.3", 4 | "downloadUrl": "https://github.com/nystudio107/recipe/archive/master.zip", 5 | "date": "2017-01-05T05:00:00.637Z", 6 | "notes": [ 7 | "[Added] Added support for 1/3, 2/3, and 1/6 fractions", 8 | "[Improved] Updated the README.md" 9 | ] 10 | }, 11 | { 12 | "version": "1.0.2", 13 | "downloadUrl": "https://github.com/nystudio107/recipe/archive/master.zip", 14 | "date": "2016-09-18T05:00:00.637Z", 15 | "notes": [ 16 | "[Fixed] Handle empty ingredients lists without erroring", 17 | "[Fixed] Handle empty directions without erroring", 18 | "[Improved] Updated the README.md" 19 | ] 20 | }, 21 | { 22 | "version": "1.0.1", 23 | "downloadUrl": "https://github.com/nystudio107/recipe/archive/master.zip", 24 | "date": "2016-05-01T05:00:00.637Z", 25 | "notes": [ 26 | "[Fixed] Fixed a minor issue with Recipe if it was embedded in a Matrix field", 27 | "[Added] Added the 'Skill Level' field", 28 | "[Improved] Updated the README.md" 29 | ] 30 | }, 31 | { 32 | "version": "1.0.0", 33 | "downloadUrl": "https://github.com/nystudio107/recipe/archive/master.zip", 34 | "date": "2016-05-01T02:56:00.637Z", 35 | "notes": [ 36 | "[Added] Initial release" 37 | ] 38 | } 39 | ] -------------------------------------------------------------------------------- /resources/js/fields/RecipeFieldType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Recipe plugin for Craft CMS 3 | * 4 | * RecipeFieldType JS 5 | * 6 | * @author nystudio107 7 | * @copyright Copyright (c) 2016 nystudio107 8 | * @link http://nystudio107.com 9 | * @package Recipe 10 | * @since 1.0.0 11 | */ 12 | 13 | ;(function ( $, window, document, undefined ) { 14 | 15 | var pluginName = "RecipeFieldType", 16 | defaults = { 17 | }; 18 | 19 | // Plugin constructor 20 | function Plugin( element, options ) { 21 | this.element = element; 22 | 23 | this.options = $.extend( {}, defaults, options) ; 24 | 25 | this._defaults = defaults; 26 | this._name = pluginName; 27 | 28 | this.init(); 29 | } 30 | 31 | Plugin.prototype = { 32 | 33 | init: function(id) { 34 | var _this = this; 35 | 36 | $(function () { 37 | 38 | /* -- _this.options gives us access to the $jsonVars that our FieldType passed down to us */ 39 | 40 | }); 41 | } 42 | }; 43 | 44 | // A really lightweight plugin wrapper around the constructor, 45 | // preventing against multiple instantiations 46 | $.fn[pluginName] = function ( options ) { 47 | return this.each(function () { 48 | if (!$.data(this, "plugin_" + pluginName)) { 49 | $.data(this, "plugin_" + pluginName, 50 | new Plugin( this, options )); 51 | } 52 | }); 53 | }; 54 | 55 | })( jQuery, window, document ); 56 | -------------------------------------------------------------------------------- /resources/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /templates/welcome.twig: -------------------------------------------------------------------------------- 1 | {% extends '_layouts/cp' %} 2 | {% set title = 'Welcome to Recipe!' %} 3 | 4 | {% set linkGetStarted = url('settings/fields') %} 5 | {% set docsUrl = "https://github.com/nystudio107/recipe/blob/master/README.md" %} 6 | 7 | {% set crumbs = [ 8 | { label: "Recipe", url: url('recipe') }, 9 | { label: "Welcome"|t, url: url('recipe/welcome') }, 10 | ] %} 11 | 12 | {% set content %} 13 |
14 | 15 |

Thanks for using Recipe!

16 |

Recipe adds a 'Recipe' FieldType for Craft CMS that you can add to any of your Sections.

17 |

In encapsulates everything you need for a recipe, including the ingredients, a photo of the recipe, directions, cooking time, ratings, and even nutritional information. It handles converting between Imperial and Metric units, outputs 'pretty' fractions for Imperial units, and can output correct ingredient portions for any number of servings.

18 |

Recipe also generates the JSON-LD microdata for your recipes if you have the SEOmatic plugin installed, which allows it to be displayed in the Google knowledge panel for search results.

19 |

We hope Recipe makes it easier for you to create and share some yummy recipes!

20 |

21 |   22 |

23 |

24 | 25 |

26 |
27 |
28 |

29 | Brought to you by nystudio107 30 |

31 |
32 | {% endset %} -------------------------------------------------------------------------------- /resources/icon-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 14 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /resources/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 21 | 22 | -------------------------------------------------------------------------------- /resources/healthy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 17 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /RecipePlugin.php: -------------------------------------------------------------------------------- 1 | request->redirect(UrlHelper::getCpUrl('recipe/welcome')); 111 | } 112 | 113 | /** 114 | */ 115 | public function onBeforeUninstall() 116 | { 117 | } 118 | 119 | /** 120 | */ 121 | public function onAfterUninstall() 122 | { 123 | } 124 | } -------------------------------------------------------------------------------- /fieldtypes/RecipeFieldType.php: -------------------------------------------------------------------------------- 1 | templates->formatInputId($name); 45 | $namespacedId = craft()->templates->namespaceInputId($id); 46 | 47 | /* -- Include our Javascript & CSS */ 48 | 49 | craft()->templates->includeCssResource('recipe/css/fields/RecipeFieldType.css'); 50 | craft()->templates->includeJsResource('recipe/js/fields/RecipeFieldType.js'); 51 | 52 | /* -- Variables to pass down to our field.js */ 53 | 54 | $jsonVars = array( 55 | 'id' => $id, 56 | 'name' => $name, 57 | 'namespace' => $namespacedId, 58 | 'prefix' => craft()->templates->namespaceInputId(""), 59 | ); 60 | 61 | $jsonVars = json_encode($jsonVars); 62 | craft()->templates->includeJs("$('#{$namespacedId}').RecipeFieldType(" . $jsonVars . ");"); 63 | 64 | /* -- Variables to pass down to our rendered template */ 65 | 66 | $variables = array( 67 | 'id' => $id, 68 | 'name' => $name, 69 | 'prefix' => craft()->templates->namespaceInputId(""), 70 | 'element' => $this->element, 71 | 'field' => $this->model, 72 | 'values' => $value 73 | ); 74 | 75 | // Whether any assets sources exist 76 | $sources = craft()->assets->findFolders(); 77 | $variables['assetsSourceExists'] = count($sources); 78 | 79 | // URL to create a new assets source 80 | $variables['newAssetsSourceUrl'] = UrlHelper::getUrl('settings/assets/sources/new'); 81 | 82 | // Set asset ID 83 | $variables['imageId'] = $value->imageId; 84 | 85 | // Set asset elements 86 | if ($variables['imageId']) 87 | { 88 | if (is_array($variables['imageId'])) 89 | { 90 | $variables['imageId'] = $variables['imageId'][0]; 91 | } 92 | $asset = craft()->elements->getElementById($variables['imageId']); 93 | $variables['elements'] = array($asset); 94 | } 95 | else 96 | { 97 | $variables['elements'] = array(); 98 | } 99 | // Set element type 100 | $variables['elementType'] = craft()->elements->getElementType(ElementType::Asset); 101 | $variables['assetSources'] = $this->getSettings()->assetSources; 102 | 103 | return craft()->templates->render('recipe/fields/RecipeFieldType.twig', $variables); 104 | } 105 | 106 | /** 107 | * @param mixed $value 108 | * @return mixed 109 | */ 110 | public function prepValueFromPost($value) 111 | { 112 | return $value; 113 | } 114 | 115 | /** 116 | * @param mixed $value 117 | * @return mixed 118 | */ 119 | public function prepValue($value) 120 | { 121 | $value = new RecipeModel($value); 122 | return $value; 123 | } 124 | 125 | 126 | /** 127 | * Define our settings 128 | * @return none 129 | */ 130 | protected function defineSettings() 131 | { 132 | return array( 133 | 'assetSources' => AttributeType::Mixed, 134 | ); 135 | } 136 | 137 | /** 138 | * Render the field settings 139 | * @return none 140 | */ 141 | public function getSettingsHtml() 142 | { 143 | $assetElementType = craft()->elements->getElementType(ElementType::Asset); 144 | return craft()->templates->render('recipe/fields/RecipeFieldType_Settings', array( 145 | 'assetSources' => $this->getElementSources($assetElementType), 146 | 'settings' => $this->getSettings() 147 | )); 148 | } 149 | 150 | 151 | /** 152 | * Returns sources avaible to an element type. 153 | * 154 | * @access protected 155 | * @return mixed 156 | */ 157 | protected function getElementSources($elementType) 158 | { 159 | $sources = array(); 160 | 161 | foreach ($elementType->getSources() as $key => $source) 162 | { 163 | if (!isset($source['heading'])) 164 | { 165 | $sources[] = array('label' => $source['label'], 'value' => $key); 166 | } 167 | } 168 | 169 | return $sources; 170 | } 171 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | # DEPRECATED 4 | 5 | This Craft CMS 2.x plugin is no longer supported, but it is fully functional, and you may continue to use it as you see fit. The license also allows you to fork it and make changes as needed for legacy support reasons. 6 | 7 | The Craft CMS 3.x version of this plugin can be found here: [craft-recipe](https://github.com/nystudio107/craft-recipe) and can also be installed via the Craft Plugin Store in the Craft CP. 8 | 9 | # Recipe plugin for Craft CMS 10 | 11 | A recipe FieldType for Craft CMS that includes microdata support 12 | 13 | Related: [Recipe for Craft 3.x](https://github.com/nystudio107/craft3-recipe) 14 | 15 | ![Screenshot](resources/screenshots/recipe01.png) 16 | 17 | ## Installation 18 | 19 | To install Recipe, follow these steps: 20 | 21 | 1. Download & unzip the file and place the `recipe` directory into your `craft/plugins` directory 22 | 2. -OR- do a `git clone https://github.com/nystudio107/recipe.git` directly into your `craft/plugins` folder. You can then update it with `git pull` 23 | 3. -OR- install with Composer via `composer require nystudio107/recipe` 24 | 4. Install plugin in the Craft Control Panel under Settings > Plugins 25 | 5. The plugin folder should be named `recipe` for Craft to see it. GitHub recently started appending `-master` (the branch name) to the name of the folder for zip file downloads. 26 | 27 | Recipe works on Craft 2.4.x, Craft 2.5.x, and Craft 2.6.x. 28 | 29 | ## Recipe Overview 30 | 31 | Recipe adds a 'Recipe' FieldType for Craft CMS that you can add to any of your Sections. 32 | 33 | In encapsulates everything you need for a recipe, including the ingredients, a photo of the recipe, directions, cooking time, ratings, and even nutritional information. It handles converting between Imperial and Metric units, outputs 'pretty' fractions for Imperial units, and can output correct ingredient portions for any number of servings. 34 | 35 | Recipe also generates the [JSON-LD microdata](https://developers.google.com/structured-data/) for your recipes if you have the [SEOmatic plugin](https://github.com/nystudio107/seomatic) installed, which allows it to be displayed in the [Google knowledge panel](https://developers.google.com/structured-data/rich-snippets/recipes) for search results. 36 | 37 | We hope Recipe makes it easier for you to create and share some yummy recipes! 38 | 39 | ## Configuring Recipe 40 | 41 | Create a Recipe field via **Settings->Fields** and you can set the Asset Sources that are used for the recipe images 42 | 43 | ## Using Recipe 44 | 45 | Once you have created the Recipe field, add it to your Section Entry Types, and fill in what recipe information is appropriate. Nothing other than the name is required, so feel free to leave anything blank that you're not using. 46 | 47 | ## Using Recipe in your Templates 48 | 49 | To display information about a recipe in your templates, you just use familiar Twig code. Let's assume the field handle for your Recipe field is `someRecipe`; this is what you'd use to output information about it: 50 | 51 | ### Basic Info 52 | 53 | * `{{ entry.someRecipe.name }}` - the name of the recipe 54 | * `{{ entry.someRecipe.description }}` - the description of the recipe 55 | * `{{ entry.someRecipe.skill }}` - the skill level required to make this recipe 56 | * `{{ entry.someRecipe.serves }}` - how many people the recipe serves 57 | * `{{ entry.someRecipe.getImageUrl() }}` - a URL to the image for the recipe 58 | * `{{ entry.someRecipe.prepTime }}` - the prep time for the recipe in minutes 59 | * `{{ entry.someRecipe.cookTime }}` - the cooking time for the recipe in minutes 60 | * `{{ entry.someRecipe.totalTime }}` - the total time for the recipe in minutes 61 | 62 | ### Ingredients 63 | 64 | For a list of ingredients, do the following (adding whatever output markup you want): 65 | 66 | {% set ingredients = entry.someRecipe.getIngredients('imperial', 1) %} 67 | {% for ingredient in ingredients %} 68 | {{ ingredient }} 69 | {% endfor %} 70 | 71 | The first parameter is the units you'd like to use (`'imperial'` or `'metric'`). The second parameter is how many people you'd like the recipe portions to be sized for. By default, it will use `'imperial'` and the serving size in the recipe if you don't pass these parameters in, e.g.: `entry.someRecipe.getIngredients()` 72 | 73 | ### Directions 74 | 75 | For a list of directions, do the following (adding whatever output markup you want): 76 | 77 | {% set directions = entry.someRecipe.getDirections() %} 78 | {% for direction in directions %} 79 | {{ direction }} 80 | {% endfor %} 81 | 82 | ### Ratings 83 | 84 | For a list of the ratings, do the following (adding whatever output markup you want): 85 | 86 | {% set ratings = entry.someRecipe.ratings %} 87 | {% for rating in ratings %} 88 | {{ rating.rating }} {{ rating.review }} {{ rating.author }} 89 | {% endfor %} 90 | 91 | For the aggregate (average) rating for this recipe, do the following (adding whatever output markup you want): 92 | 93 | {{ entry.someRecipe.getAggregateRating() }} 94 | 95 | ### Nutritional Information 96 | 97 | To output the nutritional information for the recipe, do the following: 98 | 99 | * `{{ entry.someRecipe.servingSize }}` - The serving size, in terms of the number of volume or mass 100 | * `{{ entry.someRecipe.calories }}` - The number of calories per serving 101 | * `{{ entry.someRecipe.carbohydrateContent }}` - The number of grams of carbohydrates per serving 102 | * `{{ entry.someRecipe.cholesterolContent }}` - The number of milligrams of cholesterol per serving 103 | * `{{ entry.someRecipe.fatContent }}` - The number of grams of fat per serving 104 | * `{{ entry.someRecipe.fiberContent }}` - The number of grams of fiber per serving 105 | * `{{ entry.someRecipe.proteinContent }}` - The number of grams of protein per serving 106 | * `{{ entry.someRecipe.saturatedFatContent }}` - The number of grams of saturated fat per serving 107 | * `{{ entry.someRecipe.sodiumContent }}` - The number of milligrams of sodium per serving 108 | * `{{ entry.someRecipe.sugarContent }}` - The number of grams of sugar per serving 109 | * `{{ entry.someRecipe.transFatContent }}` - The number of grams of trans fat per serving 110 | * `{{ entry.someRecipe.unsaturatedFatContent }}` - The number of grams of unsaturated fat per serving 111 | 112 | ### Image Asset ID 113 | 114 | If you need to do any further manipulation of the Recipe Image (perhaps a transform) you can get the Asset ID for it: 115 | 116 | * `{{ entry.someRecipe.imageId }}` - the Asset ID of the image for the recipe 117 | 118 | ## Rendering Recipe JSON-LD Microdata 119 | 120 | If you have the [SEOmatic plugin](https://github.com/nystudio107/seomatic) installed, Recipe can render JSON-LD microdata for you, which allows it to be displayed in the [Google knowledge panel](https://developers.google.com/structured-data/rich-snippets/recipes) for search results: 121 | 122 | {{ entry.someRecipe.renderRecipeJSONLD() }} 123 | 124 | ![Screenshot](resources/screenshots/recipe02.png) 125 | 126 | ## Recipe Roadmap 127 | 128 | Some things to do, and ideas for potential features: 129 | 130 | * Provide a front-end way to add ratings 131 | 132 | ## Recipe Changelog 133 | 134 | ### 1.0.3 -- 2017.01.05 135 | 136 | * [Added] Added support for 1/3, 2/3, and 1/6 fractions 137 | * [Improved] Updated the README.md 138 | 139 | ### 1.0.2 -- 2016.09.18 140 | 141 | * [Fixed] Handle empty ingredients lists without erroring 142 | * [Fixed] Handle empty directions without erroring 143 | * [Improved] Updated the README.md 144 | 145 | ### 1.0.1 -- 2016.05.01 146 | 147 | * [Fixed] Fixed a minor issue with Recipe if it was embedded in a Matrix field 148 | * [Added] Added the 'Skill Level' field 149 | * [Improved] Updated the README.md 150 | 151 | ### 1.0.0 -- 2016.05.01 152 | 153 | * Initial release 154 | 155 | Brought to you by [nystudio107](http://nystudio107.com) 156 | -------------------------------------------------------------------------------- /models/RecipeModel.php: -------------------------------------------------------------------------------- 1 | array(AttributeType::String, 'default' => ''), 25 | 'description' => array(AttributeType::String, 'default' => ''), 26 | 'skill' => array(AttributeType::String, 'default' => 'intermediate'), 27 | 'serves' => array(AttributeType::Number, 'default' => 1), 28 | 'ingredients' => array(AttributeType::Mixed, 'default' => ''), 29 | 'directions' => array(AttributeType::Mixed, 'default' => ''), 30 | 'imageId' => array(AttributeType::Number, 'default' => 0), 31 | 'prepTime' => array(AttributeType::Number), 32 | 'cookTime' => array(AttributeType::Number), 33 | 'totalTime' => array(AttributeType::Number), 34 | 35 | 'ratings' => array(AttributeType::Mixed), 36 | 37 | 'servingSize' => array(AttributeType::String, 'default' => ''), 38 | 'calories' => array(AttributeType::Number), 39 | 'carbohydrateContent' => array(AttributeType::Number), 40 | 'cholesterolContent' => array(AttributeType::Number), 41 | 'fatContent' => array(AttributeType::Number), 42 | 'fiberContent' => array(AttributeType::Number), 43 | 'proteinContent' => array(AttributeType::Number), 44 | 'saturatedFatContent' => array(AttributeType::Number), 45 | 'sodiumContent' => array(AttributeType::Number), 46 | 'sugarContent' => array(AttributeType::Number), 47 | 'transFatContent' => array(AttributeType::Number), 48 | 'unsaturatedFatContent' => array(AttributeType::Number), 49 | )); 50 | } 51 | 52 | /* -- Accessors ------------------------------------------------------------ */ 53 | 54 | /** 55 | * @return string the URL to the image 56 | */ 57 | public function getImageUrl() 58 | { 59 | $result = ""; 60 | if (isset($this->imageId)) 61 | { 62 | $image = craft()->assets->getFileById($this->imageId); 63 | if ($image) 64 | $result = $image->url; 65 | } 66 | return $result; 67 | } 68 | 69 | /** 70 | * @return array of strings for the ingedients 71 | */ 72 | public function getIngredients($outputUnits="imperial", $serving=0) 73 | { 74 | $result = array(); 75 | if (empty($this->ingredients)) 76 | return $result; 77 | foreach ($this->ingredients as $row) 78 | { 79 | $convertedUnits = ""; 80 | $ingredient = ""; 81 | if ($row['quantity']) 82 | { 83 | 84 | /* -- Multiply the quanity by how many servings we want */ 85 | 86 | $multiplier = 1; 87 | if ($serving > 0) 88 | $multiplier = $serving / $this->serves; 89 | $quantity = $row['quantity'] * $multiplier; 90 | $originalQuantity = $quantity; 91 | 92 | /* -- Do the units conversion */ 93 | 94 | if ($outputUnits == 'imperial') 95 | { 96 | if ($row['units'] == "mls") 97 | { 98 | $convertedUnits = "tsps"; 99 | $quantity = $quantity * 0.2; 100 | } 101 | 102 | if ($row['units'] == "ls") 103 | { 104 | $convertedUnits = "cups"; 105 | $quantity = $quantity * 4.2; 106 | } 107 | 108 | if ($row['units'] == "mgs") 109 | { 110 | $convertedUnits = "ozs"; 111 | $quantity = $quantity * 0.000035274; 112 | } 113 | 114 | if ($row['units'] == "gs") 115 | { 116 | $convertedUnits = "ozs"; 117 | $quantity = $quantity * 0.035274; 118 | } 119 | } 120 | 121 | if ($outputUnits == 'metric') 122 | { 123 | if ($row['units'] == "tsps") 124 | { 125 | $convertedUnits = "mls"; 126 | $quantity = $quantity * 4.929; 127 | } 128 | 129 | if ($row['units'] == "tbsps") 130 | { 131 | $convertedUnits = "mls"; 132 | $quantity = $quantity * 14.787; 133 | } 134 | 135 | if ($row['units'] == "flozs") 136 | { 137 | $convertedUnits = "mls"; 138 | $quantity = $quantity * 29.574; 139 | } 140 | 141 | if ($row['units'] == "cups") 142 | { 143 | $convertedUnits = "ls"; 144 | $quantity = $quantity * 0.236588; 145 | } 146 | 147 | if ($row['units'] == "ozs") 148 | { 149 | $convertedUnits = "gs"; 150 | $quantity = $quantity * 28.3495; 151 | } 152 | 153 | $quantity = round($quantity, 1); 154 | } 155 | 156 | /* -- Convert imperial units to nice fractions */ 157 | 158 | if ($outputUnits == 'imperial') 159 | $quantity = $this->convertToFractions($quantity); 160 | $ingredient .= $quantity; 161 | } 162 | if ($row['units']) 163 | { 164 | $units = $row['units']; 165 | if ($convertedUnits) 166 | $units = $convertedUnits; 167 | if ($originalQuantity <= 1) 168 | { 169 | $units = rtrim($units); 170 | $units = rtrim($units, 's'); 171 | } 172 | $ingredient .= " " . $units; 173 | } 174 | if ($row['ingredient']) 175 | $ingredient .= " " . $row['ingredient']; 176 | array_push($result, TemplateHelper::getRaw($ingredient)); 177 | } 178 | return $result; 179 | } 180 | 181 | /** 182 | * @return array of strings for the directions 183 | */ 184 | public function getDirections() 185 | { 186 | $result = array(); 187 | if (empty($this->directions)) 188 | return $result; 189 | foreach ($this->directions as $row) 190 | { 191 | $direction = $row['direction']; 192 | array_push($result, TemplateHelper::getRaw($direction)); 193 | } 194 | return $result; 195 | } 196 | 197 | /** 198 | * @return string the aggregate rating for this recipe 199 | */ 200 | public function getAggregateRating() 201 | { 202 | $result = 0; 203 | $total = 0; 204 | if (isset($this->ratings) && !empty($this->ratings)) 205 | { 206 | foreach ($this->ratings as $row) 207 | { 208 | $result += $row['rating']; 209 | $total++; 210 | } 211 | $result = $result / $total; 212 | } 213 | else 214 | $result = ""; 215 | return $result; 216 | } 217 | 218 | /** 219 | * @return string the number of ratings 220 | */ 221 | public function getRatingsCount() 222 | { 223 | $total = 0; 224 | if (isset($this->ratings) && !empty($this->ratings)) 225 | { 226 | foreach ($this->ratings as $row) 227 | { 228 | $total++; 229 | } 230 | } 231 | return $total; 232 | } 233 | 234 | /** 235 | * @return string the rendered HTML JSON-LD microdata 236 | */ 237 | public function renderRecipeJSONLD() 238 | { 239 | if (craft()->plugins->getPlugin('Seomatic')) 240 | { 241 | $metaVars = craft()->seomatic->getGlobals("", craft()->language); 242 | $recipeJSONLD = array( 243 | "type" => "Recipe", 244 | "name" => $this->name, 245 | "image" => $this->getImageUrl(), 246 | "description" => $this->description, 247 | "recipeYield" => $this->serves, 248 | "recipeIngredient" => $this->getIngredients(), 249 | "recipeInstructions" => $this->getDirections(), 250 | ); 251 | $recipeJSONLD = array_filter($recipeJSONLD); 252 | 253 | $nutrition = array( 254 | "type" => "NutritionInformation", 255 | 'servingSize' => $this->servingSize, 256 | 'calories' => $this->calories, 257 | 'carbohydrateContent' => $this->carbohydrateContent, 258 | 'cholesterolContent' => $this->cholesterolContent, 259 | 'fatContent' => $this->fatContent, 260 | 'fiberContent' => $this->fiberContent, 261 | 'proteinContent' => $this->proteinContent, 262 | 'saturatedFatContent' => $this->saturatedFatContent, 263 | 'sodiumContent' => $this->sodiumContent, 264 | 'sugarContent' => $this->sugarContent, 265 | 'transFatContent' => $this->transFatContent, 266 | 'unsaturatedFatContent' => $this->unsaturatedFatContent, 267 | ); 268 | $nutrition = array_filter($nutrition); 269 | $recipeJSONLD['nutrition'] = $nutrition; 270 | if (count($recipeJSONLD['nutrition']) == 1) 271 | unset($recipeJSONLD['nutrition']); 272 | 273 | $aggregateRating = $this->getAggregateRating(); 274 | if ($aggregateRating) 275 | { 276 | $aggregateRatings = array( 277 | "type" => "AggregateRating", 278 | 'ratingCount' => $this->getRatingsCount(), 279 | 'bestRating' => '5', 280 | 'worstRating' => '1', 281 | 'ratingValue' => $aggregateRating, 282 | ); 283 | $aggregateRatings = array_filter($aggregateRatings); 284 | $recipeJSONLD['aggregateRating'] = $aggregateRatings; 285 | 286 | $reviews = array(); 287 | foreach ($this->ratings as $rating) 288 | { 289 | $review = array( 290 | "type" => "Review", 291 | 'author' => $rating['author'], 292 | 'name' => $this->name . Craft::t(" Review"), 293 | 'description' => $rating['review'], 294 | 'reviewRating' => array( 295 | "type" => "Rating", 296 | 'bestRating' => '5', 297 | 'worstRating' => '1', 298 | 'ratingValue' => $rating['rating'], 299 | ), 300 | ); 301 | array_push($reviews, $review); 302 | } 303 | $reviews = array_filter($reviews); 304 | $recipeJSONLD['review'] = $reviews; 305 | } 306 | 307 | if ($this->prepTime) 308 | $recipeJSONLD['prepTime'] = "PT" . $this->prepTime . "M"; 309 | if ($this->cookTime) 310 | $recipeJSONLD['cookTime'] = "PT" . $this->cookTime . "M"; 311 | if ($this->totalTime) 312 | $recipeJSONLD['totalTime'] = "PT" . $this->totalTime . "M"; 313 | 314 | $recipeJSONLD['author'] = $metaVars['seomaticIdentity']; 315 | 316 | craft()->seomatic->sanitizeArray($recipeJSONLD); 317 | $result = craft()->seomatic->renderJSONLD($recipeJSONLD, false); 318 | } 319 | else 320 | $result = ""; 321 | 322 | return TemplateHelper::getRaw($result); 323 | } 324 | 325 | /** 326 | * @return string the fractionalized string 327 | */ 328 | private function convertToFractions($quantity) 329 | { 330 | $result = ""; 331 | $whole = floor($quantity); 332 | $fraction = bcdiv($quantity - $whole, 1, 2); 333 | switch ($fraction) 334 | { 335 | case 0: 336 | $fraction = ""; 337 | break; 338 | 339 | case 0.16: 340 | $fraction = " ⅙"; 341 | break; 342 | 343 | case 0.25: 344 | $fraction = " ¼"; 345 | break; 346 | 347 | case 0.33: 348 | $fraction = " ⅓"; 349 | break; 350 | 351 | case 0.5: 352 | $fraction = " ½"; 353 | break; 354 | 355 | case 0.66: 356 | $fraction = " ⅔"; 357 | break; 358 | 359 | case 0.75: 360 | $fraction = " ¾"; 361 | break; 362 | 363 | case 0.125: 364 | $fraction = " ⅛"; 365 | break; 366 | 367 | case 0.375: 368 | $fraction = " ⅜"; 369 | break; 370 | 371 | case 0.625: 372 | $fraction = " ⅝"; 373 | break; 374 | 375 | case 0.875: 376 | $fraction = " ⅞"; 377 | break; 378 | 379 | default: 380 | $precision = 5; 381 | $pnum = round($fraction, $precision); 382 | $denominator = pow(10, $precision); 383 | $numerator = $pnum * $denominator; 384 | $fraction = "" . $numerator . "" . $denominator . ""; 385 | break; 386 | } 387 | if ($whole == 0) 388 | $whole = ""; 389 | $result = $whole . $fraction; 390 | return $result; 391 | } 392 | 393 | } -------------------------------------------------------------------------------- /templates/fields/RecipeFieldType.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Recipe plugin for Craft CMS 4 | * 5 | * RecipeFieldType HTML 6 | * 7 | * @author nystudio107 8 | * @copyright Copyright (c) 2016 nystudio107 9 | * @link http://nystudio107.com 10 | * @package Recipe 11 | * @since 1.0.0 12 | */ 13 | #} 14 | 15 | {% import "_includes/forms" as forms %} 16 | 17 | {% set locale = craft.isLocalized() ? (element ? element.locale : craft.locale) %} 18 | 19 |
20 | 21 |
22 | 29 |
30 | {{ forms.textField({ 31 | id: id ~ 'name', 32 | class: 'nicetext', 33 | name: name ~ '[name]', 34 | label: 'Recipe Name' |t, 35 | instructions: 'Enter the name of this recipe' |t, 36 | value: values.name, 37 | errors: values.getErrors('name'), 38 | required: true, 39 | locale: field.translatable ? locale, 40 | }) }} 41 | 42 | {{ forms.textareaField({ 43 | id: id ~ 'description', 44 | class: 'nicetext', 45 | name: name ~ '[description]', 46 | label: 'Recipe Description' |t, 47 | instructions: 'Enter a description of this recipe' |t, 48 | value: values.description, 49 | errors: values.getErrors('description'), 50 | required: false, 51 | locale: field.translatable ? locale, 52 | }) }} 53 | 54 | {{ forms.selectField({ 55 | id: id ~ "skill", 56 | name: name ~ "[skill]", 57 | label: 'Recipe Skill' |t, 58 | instructions: 'The skill level required to make this recipe' |t, 59 | options: { 60 | "beginner": "Beginner"|t, 61 | "intermediate": "Intermediate"|t, 62 | "advanced": "Advanced"|t, 63 | }, 64 | value: values.skill, 65 | locale: field.translatable ? locale, 66 | }) }} 67 | 68 | {{ forms.textField({ 69 | id: id ~ 'serves', 70 | type: 'number', 71 | class: 'nicetext', 72 | size: 3, 73 | name: name ~ '[serves]', 74 | label: 'Recipe Serves' |t, 75 | instructions: 'Enter how many people this recipe serves' |t, 76 | value: values.serves, 77 | errors: values.getErrors('serves'), 78 | required: true, 79 | locale: field.translatable ? locale, 80 | }) }} 81 | 82 | {% if assetsSourceExists %} 83 | {{ forms.elementSelectField({ 84 | elements: elements, 85 | id: id ~ 'imageId', 86 | name: name ~ '[imageId]', 87 | label: 'Recipe Image' |t, 88 | instructions: 'Pick an image that represents this recipe' |t, 89 | elementType: elementType, 90 | criteria: { 91 | 'kind': [], 92 | 'localeEnabled': null, 93 | 'locale': locale, 94 | }, 95 | sourceElementId: imageId, 96 | sources: assetSources, 97 | jsClass: 'Craft.AssetSelectInput', 98 | addButtonLabel: "Select an Image" |t, 99 | limit: 1, 100 | locale: field.translatable ? locale, 101 | }) }} 102 | {% else %} 103 |

No assets sources currently exist. Create one now...

104 | {% endif %} 105 | 106 | {{ forms.editableTableField({ 107 | id: id ~ 'ingredients', 108 | name: name ~ '[ingredients]', 109 | label: 'Recipe Ingredients' |t, 110 | instructions: "Enter the ingredients needed for this recipe by clicking on 'Add an Ingredient'. The quantity should be in decimal form." |t, 111 | required: false, 112 | static: false, 113 | cols: { 114 | quantity: { 115 | heading: "Quantity" |t, 116 | type: "number" |t, 117 | width: '5%', 118 | }, 119 | units: { 120 | heading: "Units" |t, 121 | type: "select" |t, 122 | width: '20%', 123 | options: { 124 | "": "", 125 | "tsps": "teaspoons" |t, 126 | "tbsps": "tablespoons" |t, 127 | "flozs": "fluid ounces" |t, 128 | "cups": "cups" |t, 129 | "ozs": "ounces" |t, 130 | "mls": "milliliters" |t, 131 | "ls": "liters" |t, 132 | "mgs": "milligram" |t, 133 | "gs": "gram" |t, 134 | } 135 | }, 136 | ingredient: { 137 | heading: "Ingredient" |t, 138 | type: "singleline" |t, 139 | width: '75%', 140 | }, 141 | }, 142 | rows: values.ingredients, 143 | addRowLabel: "Add an Ingredient" |t, 144 | locale: field.translatable ? locale, 145 | }) }} 146 | 147 | {{ forms.editableTableField({ 148 | id: id ~ 'directions', 149 | name: name ~ '[directions]', 150 | label: 'Recipe Directions' |t, 151 | instructions: "Enter the directions for this recipe by clicking on 'Add a Direction'." |t, 152 | required: false, 153 | static: false, 154 | cols: { 155 | direction: { 156 | heading: "Direction" |t, 157 | type: "multiline" |t, 158 | }, 159 | }, 160 | rows: values.directions, 161 | addRowLabel: "Add a Direction" |t, 162 | }) }} 163 | 164 | {{ forms.textField({ 165 | id: id ~ 'prepTime', 166 | type: 'number', 167 | class: 'nicetext', 168 | name: name ~ '[prepTime]', 169 | size: 3, 170 | label: 'Recipe Prep Time' |t, 171 | instructions: 'The number of minutes it takes to prep this recipe' |t, 172 | value: values.prepTime, 173 | errors: values.getErrors('prepTime'), 174 | required: false, 175 | locale: field.translatable ? locale, 176 | }) }} 177 | 178 | {{ forms.textField({ 179 | id: id ~ 'cookTime', 180 | type: 'number', 181 | class: 'nicetext', 182 | size: 3, 183 | name: name ~ '[cookTime]', 184 | label: 'Recipe Cook Time' |t, 185 | instructions: 'The number of minutes it takes to cook this recipe' |t, 186 | value: values.cookTime, 187 | errors: values.getErrors('cookTime'), 188 | required: false, 189 | locale: field.translatable ? locale, 190 | }) }} 191 | 192 | {{ forms.textField({ 193 | id: id ~ 'totalTime', 194 | type: 'number', 195 | class: 'nicetext', 196 | size: 3, 197 | name: name ~ '[totalTime]', 198 | label: 'Recipe Total Time' |t, 199 | instructions: 'The number of minutes it takes for this entire recipe' |t, 200 | value: values.totalTime, 201 | errors: values.getErrors('totalTime'), 202 | required: false, 203 | locale: field.translatable ? locale, 204 | }) }} 205 |
206 | 207 | 244 | 245 | 414 | 415 |
416 | 417 |
--------------------------------------------------------------------------------