diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7d4aaa6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: aryehraber diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43f7d78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +.env +node_modules/ +package.json +mix-manifest.json +webpack.config.js +webpack.mix.js +gulpfile.js +yarn.lock diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f993f1c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Aryeh Raber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md new file mode 100755 index 0000000..102523a --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# Splash (Statamic 3) + +**Browse Unsplash images right from the CP.** + +## Installation + +Install the addon via composer: + +``` +composer require aryehraber/statamic-splash +``` + +Publish the fieldtype assets & config file: + +``` +php artisan vendor:publish --provider="AryehRaber\Splash\SplashServiceProvider" +``` + +Once installed, you will need to add an Unsplash API `Access Key` to `config/splash.php`. + +You can register for a free account via the [Unsplash Developers page](https://unsplash.com/developers). Next, simply create a "New Application" and use the Demo `Access Key`, which allows 50 requests/hour, or go through the approval process to increase to 5000 requests/hour (this is still free but takes a little more effort). Note: "requests" only refers to search queries in the CP, once an image has been selected you will be referencing the CDN link which doesn't go through the API. + +## Usage + +Simply add a field inside your blueprint and use `type: splash` to get started. + +**Blueprint** + +```yaml +fields: + - + handle: hero_image + field: + type: splash +``` + +After selecting an image and saving your content, the full Unsplash response data will be accessible in templates using Antlers. The image itself *will not* be saved inside your Asset Container, instead you will be referencing images using Unsplash's CDN, also called Hotlinking (this is required by their [API Guidelines](https://help.unsplash.com/en/articles/2511271-guideline-hotlinking-images)). + +The benefit here is that it takes a significant load off your server, since images are often the heaviest assets on a page. Additionally, since Unsplash's CDN is spread worldwide, images will load super fast regardless of the visitor's location. + +## Tags + +### Raw + +`{{ splash:raw :image="field_name" }}` + +This tag outputs the CDN link to the image's `raw` URL. Due to the high-quality nature of Unsplash photos, images are often 10-20MB, which is overkill for _most_ websites; you can therefore use a few Glide-like parameters to request exactly what you need (more info below). + +**Example** + +```html +{{ splash:raw :image="hero_image" width="2000" quality="80" }} +``` + +--- + +### Image + +`{{ splash:image :image="field_name" }}` or `{{ splash:field_name }}` + +This tag also outputs the CDN link to the image's `raw` URL, but includes a few sensible defaults to reduce the image filesize a whole lot. + +Any of these params can be overriden by using the parameters listed below, the defaults used are: + +```php +'q' => '80', +'w' => '1500', +'fit' => 'crop', +'crop' => 'faces,edges', +'auto' => 'format', +``` + +**Example** + +```html +{{ splash:image :image="hero_image" width="1000" }} +``` +Or using the shorthand: +```html +{{ splash:hero_image width="1000" }} +``` + +--- + +### Attribution + +`{{ splash:attribution :image="field_name" }}` + +This tag outputs the Unsplash attribution text from their [API Guidelines](https://help.unsplash.com/en/articles/2511315-guideline-attribution), including links to the photographer's profile and Unsplash website. + +**Example** + +```html +{{ splash:attribution :image="hero_image" }} +``` + +**Output** + +```html +Photo by Annie Spratt on Unsplash +``` + +Since the photo's meta data is stored inside your content, you can also loop over the photographer's data using Antlers: + +```html +{{ hero_image:user }} + +{{ /hero_image:user }} +``` + +### Parameters + +Splash offers a number of Glide-like parameters to transform images to your needs. You can pass any [Unsplash parameter](https://unsplash.com/documentation#supported-parameters) into the `raw` and `image` tags to get started. Just like when using Glide within Statamic, you can also use the easier-to-read alias parameters below: + +| Param | Description | +|-------|-------------| +| `width` | Sets the width (in pixels). | +| `height` | Sets the height (in pixels). | +| `square` | Shortcut to set width and height to the same value. | +| `quality` | Sets the compression quality (value between 0 and 100). | +| `format` | Encodes the image to a specific format (recommended to use `auto="format"` instead to auto-pick based on visitor's browser). | +| `dpr` | Adjusts the device pixel ratio of the image. | +| `fit` | Changes the fit of the image within the specified dimensions ([see all available values on Imgix](https://docs.imgix.com/apis/url/size/fit)). | +| `crop` | Applies cropping to the image ([see all available values on Imgix](https://docs.imgix.com/apis/url/size/crop)). | +| `auto` | Automates a baseline level of optimization for the image ([see all available values on Imgix](https://docs.imgix.com/apis/url/auto/auto)). | diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..029763e --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "aryehraber/statamic-splash", + "type": "statamic-addon", + "description": "Browse Unsplash images right from the CP.", + "keywords": [ + "statamic", + "unsplash", + "splash" + ], + "homepage": "https://github.com/aryehraber/statamic-splash", + "license": "MIT", + "require": { + "statamic/cms": "3.0.*@beta" + }, + "autoload": { + "psr-4": { + "AryehRaber\\Splash\\": "src" + } + }, + "authors": [ + { + "name": "Aryeh Raber", + "email": "aryeh.raber@gmail.com", + "homepage": "https://aryeh.dev", + "role": "Developer" + } + ], + "extra": { + "statamic": { + "name": "Splash", + "description": "Browse Unsplash images right from the CP.", + "version": "1.0.0" + }, + "laravel": { + "providers": [ + "AryehRaber\\Splash\\SplashServiceProvider" + ] + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/splash.php b/config/splash.php new file mode 100644 index 0000000..a83df9f --- /dev/null +++ b/config/splash.php @@ -0,0 +1,6 @@ + env('UNSPLASH_KEY', ''), + 'default_thumb_size' => 'small', +]; diff --git a/resources/dist/js/splash-fieldtype.js b/resources/dist/js/splash-fieldtype.js new file mode 100644 index 0000000..c4603c2 --- /dev/null +++ b/resources/dist/js/splash-fieldtype.js @@ -0,0 +1 @@ +!function(t){var e={};function n(s){if(e[s])return e[s].exports;var i=e[s]={i:s,l:!1,exports:{}};return t[s].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,s){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:s})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var s=Object.create(null);if(n.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(s,i,function(e){return t[e]}.bind(null,i));return s},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="/",n(n.s=1)}([function(t,e,n){var s=n(3);"string"==typeof s&&(s=[[t.i,s,""]]);var i={hmr:!0,transform:void 0,insertInto:void 0};n(5)(s,i);s.locals&&(t.exports=s.locals)},function(t,e,n){t.exports=n(7)},function(t,e,n){"use strict";var s=n(0);n.n(s).a},function(t,e,n){(t.exports=n(4)(!1)).push([t.i,"\n.-z-1 { z-index: -1 !important;\n}\n.lazyloaded { transition: all 0.3s ease;\n}\n",""])},function(t,e){t.exports=function(t){var e=[];return e.toString=function(){return this.map((function(e){var n=function(t,e){var n=t[1]||"",s=t[3];if(!s)return n;if(e&&"function"==typeof btoa){var i=(a=s,"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(a))))+" */"),r=s.sources.map((function(t){return"/*# sourceURL="+s.sourceRoot+t+" */"}));return[n].concat(r).concat([i]).join("\n")}var a;return[n].join("\n")}(e,t);return e[2]?"@media "+e[2]+"{"+n+"}":n})).join("")},e.i=function(t,n){"string"==typeof t&&(t=[[null,t,""]]);for(var s={},i=0;i=0&&f.splice(e,1)}function g(t){var e=document.createElement("style");if(void 0===t.attrs.type&&(t.attrs.type="text/css"),void 0===t.attrs.nonce){var s=function(){0;return n.nc}();s&&(t.attrs.nonce=s)}return b(e,t.attrs),m(t,e),e}function b(t,e){Object.keys(e).forEach((function(n){t.setAttribute(n,e[n])}))}function w(t,e){var n,s,i,r;if(e.transform&&t.css){if(!(r="function"==typeof e.transform?e.transform(t.css):e.transform.default(t.css)))return function(){};t.css=r}if(e.singleton){var a=u++;n=c||(c=g(e)),s=_.bind(null,n,a,!1),i=_.bind(null,n,a,!0)}else t.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=function(t){var e=document.createElement("link");return void 0===t.attrs.type&&(t.attrs.type="text/css"),t.attrs.rel="stylesheet",b(e,t.attrs),m(t,e),e}(e),s=S.bind(null,n,e),i=function(){v(n),n.href&&URL.revokeObjectURL(n.href)}):(n=g(e),s=C.bind(null,n),i=function(){v(n)});return s(t),function(e){if(e){if(e.css===t.css&&e.media===t.media&&e.sourceMap===t.sourceMap)return;s(t=e)}else i()}}t.exports=function(t,e){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(e=e||{}).attrs="object"==typeof e.attrs?e.attrs:{},e.singleton||"boolean"==typeof e.singleton||(e.singleton=a()),e.insertInto||(e.insertInto="head"),e.insertAt||(e.insertAt="bottom");var n=p(t,e);return h(n,e),function(t){for(var s=[],i=0;i0&&void 0!==arguments[0]&&arguments[0];if(!this.loading){this.loading=!0,this.error=null,e&&this.searchPage++;var n="https://api.unsplash.com",s=this.searchQuery?"/search/photos":"/photos",i={client_id:this.meta.access_key,query:this.searchQuery,page:this.searchPage,per_page:30};this.$axios.get("".concat(n).concat(s),{params:i}).then((function(n){var s=n.data,i=s.results||s;t.images=e?t.images.concat(i):i,t.hasNextPage=s.total_pages?s.total_pages>t.searchPage:null,t.loading=!1})).catch((function(e){if(e.response){var n=e.response,s=n.data,i=n.status;t.error={data:s,status:i}}}))}},loadMore:function(){this.search(!0)},openBrowser:function(){this.showBrowser=!0,this.search()},closeBrowser:function(){this.showBrowser=!1,this.images=[],this.searchQuery="",this.searchPage=1,this.hasNextPage=null,this.selectedImage=null},openImage:function(t){this.selectedImage=t},closeImage:function(){this.selectedImage=null},select:function(){this.$emit("input",this.selectedImage),this.closeBrowser()},removeImage:function(){this.selectedImage=null,this.$emit("input",null)},setDefaultThumbSize:function(){void 0!==this.config.thumb_size?this.selectedThumbSize=this.config.thumb_size:void 0!==this.meta.default_thumb_size&&(this.selectedThumbSize=this.meta.default_thumb_size)},initInfiniteScroll:function(){var t=this,e=this.$refs.imageContainer;e&&e.addEventListener("scroll",_.throttle((function(){if(!t.loading){t.$refs.loadMoreButton&&e.scrollTop+e.clientHeight>=e.scrollHeight-300&&t.loadMore()}}),250))}},watch:{showBrowser:function(t){t&&setTimeout(this.initInfiniteScroll,100)},searchQuery:function(){this.images=[],this.searchPage=1,this.hasNextPage=null,this.selectedImage=null,this.search()}},created:function(){this.setDefaultThumbSize()}},l=(n(2),s(o,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.meta.access_key?n("div",[n("input-field",{attrs:{value:t.value},on:{open:t.openBrowser,remove:t.removeImage}}),t._v(" "),t.showBrowser?n("stack",{attrs:{name:"unsplash-browser"},on:{closed:t.closeBrowser}},[n("div",{staticClass:"flex flex-col h-full bg-white"},[n("div",{staticClass:"relative flex flex-col h-full"},[t.selectedImage?n("image-viewer",{attrs:{image:t.selectedImage,"thumb-width":t.thumbWidth,"thumb-sizes":t.thumbSizes},on:{close:t.closeImage}}):t._e(),t._v(" "),n("div",{staticClass:"flex items-center justify-between w-full p-2 bg-white"},[n("data-list-search",{attrs:{placeholder:"Search Unsplash..."},model:{value:t.searchQuery,callback:function(e){t.searchQuery=e},expression:"searchQuery"}}),t._v(" "),n("div",{staticClass:"hidden md:flex ml-1"},[n("button",{staticClass:"btn btn-icon icon",class:"large"===t.selectedThumbSize?"icon-resize-100":"icon-resize-full-screen",on:{click:function(e){t.selectedThumbSize="large"===t.selectedThumbSize?"small":"large"}}})])],1),t._v(" "),n("div",{ref:"imageContainer",staticClass:"relative flex-1 w-full h-full",class:{"overflow-y-scroll":!t.selectedImage}},[n("div",{staticClass:"absolute pin p-2"},[n("div",{staticClass:"flex flex-wrap -mx-1"},t._l(t.filteredImages,(function(e){return n("thumb",{key:e.id,attrs:{image:e,width:t.thumbWidth,sizes:t.thumbSizes},on:{open:function(n){return t.openImage(e)}}})})),1),t._v(" "),t.canLoadMore?n("div",{staticClass:"z-20 p-2 pb-4 text-center"},[n("button",{ref:"loadMoreButton",staticClass:"btn",attrs:{disabled:t.loading},domProps:{textContent:t._s(t.loading?"Loading...":"Load More")},on:{click:t.loadMore}})]):t._e(),t._v(" "),t.error?n("div",{staticClass:"pb-5 font-medium text-center text-red",domProps:{textContent:t._s("Unsplash Error: "+t.error.data+" ("+t.error.status+")")}}):t._e()])])],1),t._v(" "),n("div",{staticClass:"flex items-center justify-end z-20 p-2 bg-grey-20 border-t"},[n("button",{staticClass:"btn",on:{click:t.closeBrowser}},[t._v("\n Cancel\n ")]),t._v(" "),n("button",{staticClass:"btn-primary ml-1",attrs:{disabled:!t.selectedImage},on:{click:t.select}},[t._v("\n Select\n ")])])])]):t._e()],1):n("div",{staticClass:"text-sm"},[n("code",[t._v("Missing Unsplash API Access Key")])])}),[],!1,null,null,null).exports);Statamic.$components.register("splash-fieldtype",l)}]); \ No newline at end of file diff --git a/resources/js/Splash.vue b/resources/js/Splash.vue new file mode 100644 index 0000000..f9aa4c2 --- /dev/null +++ b/resources/js/Splash.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/resources/js/components/ImageViewer.vue b/resources/js/components/ImageViewer.vue new file mode 100644 index 0000000..f2c5bdc --- /dev/null +++ b/resources/js/components/ImageViewer.vue @@ -0,0 +1,46 @@ + + + diff --git a/resources/js/components/InputField.vue b/resources/js/components/InputField.vue new file mode 100644 index 0000000..0916ad5 --- /dev/null +++ b/resources/js/components/InputField.vue @@ -0,0 +1,37 @@ + + + diff --git a/resources/js/components/Splash.vue b/resources/js/components/Splash.vue new file mode 100644 index 0000000..be9bc4f --- /dev/null +++ b/resources/js/components/Splash.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/resources/js/components/Thumb.vue b/resources/js/components/Thumb.vue new file mode 100644 index 0000000..bc2a281 --- /dev/null +++ b/resources/js/components/Thumb.vue @@ -0,0 +1,47 @@ + + + diff --git a/resources/js/splash.js b/resources/js/splash.js new file mode 100644 index 0000000..9b7b0f1 --- /dev/null +++ b/resources/js/splash.js @@ -0,0 +1,3 @@ +import Splash from './components/Splash.vue' + +Statamic.$components.register('splash-fieldtype', Splash) diff --git a/resources/views/attribution.blade.php b/resources/views/attribution.blade.php new file mode 100644 index 0000000..b09a588 --- /dev/null +++ b/resources/views/attribution.blade.php @@ -0,0 +1 @@ +Photo by {{ $userName }} on Unsplash diff --git a/src/SplashFieldtype.php b/src/SplashFieldtype.php new file mode 100644 index 0000000..97fc8f8 --- /dev/null +++ b/src/SplashFieldtype.php @@ -0,0 +1,17 @@ +loadViewsFrom(__DIR__.'/../resources/views', 'splash'); + + $this->mergeConfigFrom(__DIR__.'/../config/splash.php', 'splash'); + + $this->publishes([ + __DIR__.'/../config/splash.php' => config_path('splash.php'), + ], 'config'); + } +} diff --git a/src/SplashTags.php b/src/SplashTags.php new file mode 100644 index 0000000..151439a --- /dev/null +++ b/src/SplashTags.php @@ -0,0 +1,110 @@ + '80', + 'w' => '1500', + 'fit' => 'crop', + 'crop' => 'faces,edges', + 'auto' => 'format', + ]; + + public function wildcard($handle) + { + $this->setImage(array_get($this->context, $handle)); + + return $this->image(); + } + + public function image() + { + return $this->raw($this->defaultParams); + } + + public function raw($params = []) + { + $this->setImage(); + + if (! $image = $this->getImage()) return; + + $url = $image['urls']['raw']; + $query = $this->buildQuery($this->getParams($params)); + + return $url . $query; + } + + public function attribution() + { + $this->setImage(); + + if (! $image = $this->getImage()) return; + + $user = $image['user']; + $newTab = $this->getBool('new_tab', true); + + return view('splash::attribution', [ + 'userName' => $user['name'] ?? '', + 'userProfileUrl' => $user['links']['html'] ?? '', + 'utm' => 'utm_source=statamic_splash&utm_medium=referral', + 'linkTarget' => $newTab ? ' target="_blank" rel="noopener"' : '', + ])->render(); + } + + protected function setImage($image = null) + { + $this->image = $image ?: $this->image ?: $this->get('image'); + + return $this; + } + + protected function getImage() + { + if ($this->image instanceof Value) { + $image = $this->image->value(); + } + + if (is_array($this->image)) { + $image = $this->image; + } + + return $image ?? null; + } + + protected function getParams($defaults = []) + { + $params = array_filter([ + 'w' => $this->get(['w', 'width']), + 'h' => $this->get(['h', 'height']), + 'q' => $this->get(['q', 'quality']), + 'fm' => $this->get(['fm', 'format']), + 'dpr' => $this->get('dpr'), + 'fit' => $this->get('fit'), + 'crop' => $this->get('crop'), + 'auto' => $this->get('auto'), + ]); + + if ($size = $this->get('square')) { + $params['w'] = $size; + $params['h'] = $size; + } + + return array_merge($defaults, $params); + } + + protected function buildQuery($params = []) + { + if (empty($params)) return; + + return '&'.http_build_query($params); + } +}