├── .gitignore ├── README.md ├── composer.json ├── config └── nova-blog.php ├── dist ├── css │ ├── post-form.css │ └── tool.css ├── js │ ├── post-form.js │ └── tool.js └── mix-manifest.json ├── package.json ├── resources ├── js │ ├── components │ │ ├── Tool.vue │ │ └── post-form │ │ │ ├── Create.vue │ │ │ ├── DefaultField.vue │ │ │ ├── Edit.vue │ │ │ └── FieldSet.vue │ ├── post-form.js │ └── tool.js ├── sass │ ├── post-form.scss │ └── tool.scss └── views │ └── navigation.blade.php ├── routes └── api.php ├── src ├── Actions │ ├── PublishPost.php │ └── SaveDraftPost.php ├── BlogResponder.php ├── Bootstrap │ └── Blog.php ├── FieldSet.php ├── Http │ ├── Controllers │ │ ├── BlogBaseController.php │ │ ├── DashboardController.php │ │ ├── MigrationController.php │ │ ├── ResetController.php │ │ └── UninstallController.php │ └── Middleware │ │ └── Authorize.php ├── Metrics │ └── Posts │ │ ├── NewPosts.php │ │ └── PostsTrend.php ├── Migrations │ ├── CategoryMigration.php │ ├── CommentMigration.php │ ├── ForeignKeyMigration.php │ ├── Migrator.php │ ├── NovaPendingTrixAttachmentsMigration.php │ ├── NovaTrixAttachmentsMigration.php │ ├── PostCategoryPivotMigration.php │ ├── PostMigration.php │ └── TagMigration.php ├── Models │ ├── Category.php │ ├── Comment.php │ ├── Image.php │ ├── Post.php │ └── Tag.php ├── NovaBlogTool.php ├── Observers │ └── ImageObserver.php ├── Policies │ ├── CategoryPolicy.php │ ├── CommentPolicy.php │ ├── PostPolicy.php │ └── TagPolicy.php ├── PostForm.php ├── Processors │ └── StoreImage.php ├── Resources │ ├── Category.php │ ├── Comment.php │ ├── Post.php │ ├── Resource.php │ └── Tag.php ├── Scopes │ └── PublishedScope.php ├── ToolServiceProvider.php └── Traits │ └── Sluggable.php └── webpack.mix.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | package-lock.json 5 | composer.phar 6 | composer.lock 7 | phpunit.xml 8 | .phpunit.result.cache 9 | .DS_Store 10 | Thumbs.db 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nova Blog Tool. 2 | 3 | This tool allow you to create a simple blog for your website using Laravel Nova. 4 | 5 | ## Installation 6 | 7 | You can install the package via composer: 8 | 9 | ```bash 10 | composer require digitalcloud/nova-blog-tool 11 | ``` 12 | 13 | ## Usage 14 | 15 | You must register the tool with Nova. This is typically done in the tools method of the NovaServiceProvider, in app/Providers/NovaServiceProvider.php. 16 | 17 | ```php 18 | 19 | use DigitalCloud\NovaBlogTool\NovaBlogTool; 20 | // .... 21 | 22 | public function tools() 23 | { 24 | return [ 25 | // ... 26 | new NovaBlogTool(), 27 | // ... 28 | ]; 29 | } 30 | 31 | ``` 32 | 33 | ## Images 34 | 35 | ![blog](https://user-images.githubusercontent.com/41853913/50156770-49735f00-02d8-11e9-8a09-10047d80d551.PNG) 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digitalcloud/nova-blog-tool", 3 | "description": "A Laravel Nova tool.", 4 | "keywords": [ 5 | "laravel", 6 | "nova" 7 | ], 8 | "license": "MIT", 9 | "require": { 10 | "php": ">=7.1.0", 11 | "spatie/laravel-sluggable": "^2.1", 12 | "spatie/laravel-translatable": "*", 13 | "spatie/nova-tags-field": "*", 14 | "digitalcloud/multilingual-nova": "^1.0", 15 | "infinety-es/nova-filemanager": "^1.1", 16 | "fourstacks/nova-checkboxes": "^0.1.0", 17 | "yassi/nova-custom-form": "dev-master" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "DigitalCloud\\NovaBlogTool\\": "src/" 22 | } 23 | }, 24 | "extra": { 25 | "laravel": { 26 | "providers": [ 27 | "DigitalCloud\\NovaBlogTool\\ToolServiceProvider", 28 | "Yassi\\NovaCustomForm\\NovaCustomFormServiceProvider" 29 | ] 30 | } 31 | }, 32 | "config": { 33 | "sort-packages": true 34 | }, 35 | "minimum-stability": "dev", 36 | "prefer-stable": true 37 | } 38 | -------------------------------------------------------------------------------- /config/nova-blog.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'users' => [ 6 | 'model' => env('BLOG_USER_MODEL', App\User::class), 7 | ], 8 | 9 | 'posts' => [ 10 | 'search' => ['id', 'title', 'content'], 11 | ], 12 | 13 | 'categories' => [ 14 | 'search' => ['id', 'name', 'description'], 15 | ], 16 | 17 | 'comments' => [ 18 | 'search' => ['id', 'name'], 19 | ], 20 | 21 | 'tags' => [ 22 | 'search' => ['id', 'name'], 23 | ], 24 | ], 25 | 26 | 'user_model' => env('BLOG_USER_MODEL', App\User::class), 27 | ]; 28 | -------------------------------------------------------------------------------- /dist/css/post-form.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalCloud/nova-blog-tool/49ad4bd80a85a1acee7e1b42782d9f58ac92c8e6/dist/css/post-form.css -------------------------------------------------------------------------------- /dist/css/tool.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalCloud/nova-blog-tool/49ad4bd80a85a1acee7e1b42782d9f58ac92c8e6/dist/css/tool.css -------------------------------------------------------------------------------- /dist/js/tool.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 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 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // getDefaultExport function for compatibility with non-harmony modules 48 | /******/ __webpack_require__.n = function(module) { 49 | /******/ var getter = module && module.__esModule ? 50 | /******/ function getDefault() { return module['default']; } : 51 | /******/ function getModuleExports() { return module; }; 52 | /******/ __webpack_require__.d(getter, 'a', getter); 53 | /******/ return getter; 54 | /******/ }; 55 | /******/ 56 | /******/ // Object.prototype.hasOwnProperty.call 57 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 58 | /******/ 59 | /******/ // __webpack_public_path__ 60 | /******/ __webpack_require__.p = ""; 61 | /******/ 62 | /******/ // Load entry module and return exports 63 | /******/ return __webpack_require__(__webpack_require__.s = 40); 64 | /******/ }) 65 | /************************************************************************/ 66 | /******/ ({ 67 | 68 | /***/ 2: 69 | /***/ (function(module, exports) { 70 | 71 | /* globals __VUE_SSR_CONTEXT__ */ 72 | 73 | // IMPORTANT: Do NOT use ES2015 features in this file. 74 | // This module is a runtime utility for cleaner component module output and will 75 | // be included in the final webpack user bundle. 76 | 77 | module.exports = function normalizeComponent ( 78 | rawScriptExports, 79 | compiledTemplate, 80 | functionalTemplate, 81 | injectStyles, 82 | scopeId, 83 | moduleIdentifier /* server only */ 84 | ) { 85 | var esModule 86 | var scriptExports = rawScriptExports = rawScriptExports || {} 87 | 88 | // ES6 modules interop 89 | var type = typeof rawScriptExports.default 90 | if (type === 'object' || type === 'function') { 91 | esModule = rawScriptExports 92 | scriptExports = rawScriptExports.default 93 | } 94 | 95 | // Vue.extend constructor export interop 96 | var options = typeof scriptExports === 'function' 97 | ? scriptExports.options 98 | : scriptExports 99 | 100 | // render functions 101 | if (compiledTemplate) { 102 | options.render = compiledTemplate.render 103 | options.staticRenderFns = compiledTemplate.staticRenderFns 104 | options._compiled = true 105 | } 106 | 107 | // functional template 108 | if (functionalTemplate) { 109 | options.functional = true 110 | } 111 | 112 | // scopedId 113 | if (scopeId) { 114 | options._scopeId = scopeId 115 | } 116 | 117 | var hook 118 | if (moduleIdentifier) { // server build 119 | hook = function (context) { 120 | // 2.3 injection 121 | context = 122 | context || // cached call 123 | (this.$vnode && this.$vnode.ssrContext) || // stateful 124 | (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional 125 | // 2.2 with runInNewContext: true 126 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { 127 | context = __VUE_SSR_CONTEXT__ 128 | } 129 | // inject component styles 130 | if (injectStyles) { 131 | injectStyles.call(this, context) 132 | } 133 | // register component module identifier for async chunk inferrence 134 | if (context && context._registeredComponents) { 135 | context._registeredComponents.add(moduleIdentifier) 136 | } 137 | } 138 | // used by ssr in case component is cached and beforeCreate 139 | // never gets called 140 | options._ssrRegister = hook 141 | } else if (injectStyles) { 142 | hook = injectStyles 143 | } 144 | 145 | if (hook) { 146 | var functional = options.functional 147 | var existing = functional 148 | ? options.render 149 | : options.beforeCreate 150 | 151 | if (!functional) { 152 | // inject component registration as beforeCreate hook 153 | options.beforeCreate = existing 154 | ? [].concat(existing, hook) 155 | : [hook] 156 | } else { 157 | // for template-only hot-reload because in that case the render fn doesn't 158 | // go through the normalizer 159 | options._injectStyles = hook 160 | // register for functioal component in vue file 161 | options.render = function renderWithStyleInjection (h, context) { 162 | hook.call(context) 163 | return existing(h, context) 164 | } 165 | } 166 | } 167 | 168 | return { 169 | esModule: esModule, 170 | exports: scriptExports, 171 | options: options 172 | } 173 | } 174 | 175 | 176 | /***/ }), 177 | 178 | /***/ 40: 179 | /***/ (function(module, exports, __webpack_require__) { 180 | 181 | __webpack_require__(41); 182 | __webpack_require__(45); 183 | module.exports = __webpack_require__(46); 184 | 185 | 186 | /***/ }), 187 | 188 | /***/ 41: 189 | /***/ (function(module, exports, __webpack_require__) { 190 | 191 | Nova.booting(function (Vue, router) { 192 | router.addRoutes([{ 193 | name: 'nova-blog-tool', 194 | path: '/nova-blog-tool', 195 | component: __webpack_require__(42) 196 | }]); 197 | }); 198 | 199 | /***/ }), 200 | 201 | /***/ 42: 202 | /***/ (function(module, exports, __webpack_require__) { 203 | 204 | var disposed = false 205 | var normalizeComponent = __webpack_require__(2) 206 | /* script */ 207 | var __vue_script__ = __webpack_require__(43) 208 | /* template */ 209 | var __vue_template__ = __webpack_require__(44) 210 | /* template functional */ 211 | var __vue_template_functional__ = false 212 | /* styles */ 213 | var __vue_styles__ = null 214 | /* scopeId */ 215 | var __vue_scopeId__ = null 216 | /* moduleIdentifier (server only) */ 217 | var __vue_module_identifier__ = null 218 | var Component = normalizeComponent( 219 | __vue_script__, 220 | __vue_template__, 221 | __vue_template_functional__, 222 | __vue_styles__, 223 | __vue_scopeId__, 224 | __vue_module_identifier__ 225 | ) 226 | Component.options.__file = "resources/js/components/Tool.vue" 227 | 228 | /* hot reload */ 229 | if (false) {(function () { 230 | var hotAPI = require("vue-hot-reload-api") 231 | hotAPI.install(require("vue"), false) 232 | if (!hotAPI.compatible) return 233 | module.hot.accept() 234 | if (!module.hot.data) { 235 | hotAPI.createRecord("data-v-68ff5483", Component.options) 236 | } else { 237 | hotAPI.reload("data-v-68ff5483", Component.options) 238 | } 239 | module.hot.dispose(function (data) { 240 | disposed = true 241 | }) 242 | })()} 243 | 244 | module.exports = Component.exports 245 | 246 | 247 | /***/ }), 248 | 249 | /***/ 43: 250 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 251 | 252 | "use strict"; 253 | Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); 254 | // 255 | // 256 | // 257 | // 258 | // 259 | // 260 | // 261 | // 262 | // 263 | // 264 | // 265 | // 266 | // 267 | // 268 | // 269 | // 270 | // 271 | // 272 | // 273 | // 274 | // 275 | // 276 | // 277 | // 278 | // 279 | // 280 | // 281 | // 282 | // 283 | // 284 | // 285 | // 286 | // 287 | // 288 | // 289 | // 290 | 291 | /* harmony default export */ __webpack_exports__["default"] = ({ 292 | data: function data() { 293 | return { 294 | installed: null, 295 | messages: [], 296 | error: false 297 | }; 298 | }, 299 | mounted: function mounted() { 300 | this.installationCheck(); 301 | }, 302 | 303 | 304 | methods: { 305 | installationCheck: function installationCheck() { 306 | var _this = this; 307 | 308 | Nova.request().get("/nova-vendor/nova-blog-tool/check-migrations").then(function (response) { 309 | return _this.installed = response.data.installed; 310 | }).catch(function () { 311 | return _this.error = true; 312 | }); 313 | }, 314 | install: function install() { 315 | var _this2 = this; 316 | 317 | Nova.request().get("/nova-vendor/nova-blog-tool/migrate-tables").then(function (response) { 318 | return _this2.messages = response.data.messages; 319 | }).then(function () { 320 | return _this2.reloadPage(); 321 | }).then(function () { 322 | return _this2.installationCheck(); 323 | }).catch(function (error) { 324 | return _this2.error = true; 325 | }); 326 | }, 327 | resetContent: function resetContent() { 328 | var _this3 = this; 329 | 330 | this.resetMessages(); 331 | 332 | Nova.request().get("/nova-vendor/nova-blog-tool/reset-content").then(function (response) { 333 | return _this3.messages = response.data.messages; 334 | }).then(function () { 335 | setTimeout(function () { 336 | _this3.messages = []; 337 | }, 2000); 338 | }).catch(function (error) { 339 | return _this3.error = true; 340 | }); 341 | }, 342 | deleteTables: function deleteTables() { 343 | var _this4 = this; 344 | 345 | this.resetMessages(); 346 | 347 | Nova.request().get("/nova-vendor/nova-blog-tool/uninstall").then(function (response) { 348 | return _this4.messages = response.data.messages; 349 | }).then(function () { 350 | return _this4.reloadPage(); 351 | }).catch(function (error) { 352 | return _this4.error = true; 353 | }); 354 | }, 355 | resetMessages: function resetMessages() { 356 | this.messages = []; 357 | }, 358 | reloadPage: function reloadPage() { 359 | setTimeout(function () { 360 | window.location.reload(); 361 | }, 1000); 362 | } 363 | } 364 | }); 365 | 366 | /***/ }), 367 | 368 | /***/ 44: 369 | /***/ (function(module, exports, __webpack_require__) { 370 | 371 | var render = function() { 372 | var _vm = this 373 | var _h = _vm.$createElement 374 | var _c = _vm._self._c || _h 375 | return _c( 376 | "div", 377 | [ 378 | _c("heading", { staticClass: "mb-6" }, [_vm._v("Blog")]), 379 | _vm._v(" "), 380 | _c( 381 | "card", 382 | [ 383 | _vm.messages.length !== 0 384 | ? [ 385 | _c("div", { staticClass: "px-4 py-4 " }, [ 386 | _c( 387 | "div", 388 | { 389 | staticClass: 390 | "mx-2 my-2 px-6 py-8 relative bg-80 text-20 rounded-lg" 391 | }, 392 | [ 393 | _c( 394 | "div", 395 | { staticClass: "absolute pin-t pin-r mr-2 mt-2 pr-1" }, 396 | [ 397 | _c( 398 | "button", 399 | { 400 | staticClass: "text-20 font-bold", 401 | on: { click: _vm.resetMessages } 402 | }, 403 | [_c("strong", [_vm._v("X")])] 404 | ) 405 | ] 406 | ), 407 | _vm._v(" "), 408 | _vm._l(_vm.messages, function(message, index) { 409 | return _c("div", { key: index }, [ 410 | _c("p", [_vm._v(_vm._s(message))]) 411 | ]) 412 | }) 413 | ], 414 | 2 415 | ) 416 | ]) 417 | ] 418 | : _vm._e(), 419 | _vm._v(" "), 420 | _c( 421 | "div", 422 | { staticClass: "px-6 py-8 text-80" }, 423 | [ 424 | _vm.installed 425 | ? [ 426 | _c("h4", { staticClass: "mb-4" }, [ 427 | _vm._v("Blog is currently installed and active.") 428 | ]), 429 | _vm._v(" "), 430 | _c( 431 | "button", 432 | { 433 | staticClass: "btn btn-default btn-danger", 434 | on: { click: _vm.deleteTables } 435 | }, 436 | [_vm._v("Uninstall and delete blog tables")] 437 | ), 438 | _vm._v(" "), 439 | _c( 440 | "button", 441 | { 442 | staticClass: 443 | "btn btn-default bg-80 text-20 font-normal", 444 | on: { click: _vm.resetContent } 445 | }, 446 | [_vm._v("Reset blog content")] 447 | ) 448 | ] 449 | : [ 450 | _c("p", [ 451 | _c("span", { staticClass: "pr-4" }, [ 452 | _vm._v("Blog DB tables not found.") 453 | ]), 454 | _vm._v(" "), 455 | _c( 456 | "button", 457 | { 458 | staticClass: "btn btn-default btn-primary", 459 | on: { click: _vm.install } 460 | }, 461 | [_vm._v("Install")] 462 | ) 463 | ]) 464 | ] 465 | ], 466 | 2 467 | ) 468 | ], 469 | 2 470 | ) 471 | ], 472 | 1 473 | ) 474 | } 475 | var staticRenderFns = [] 476 | render._withStripped = true 477 | module.exports = { render: render, staticRenderFns: staticRenderFns } 478 | if (false) { 479 | module.hot.accept() 480 | if (module.hot.data) { 481 | require("vue-hot-reload-api") .rerender("data-v-68ff5483", module.exports) 482 | } 483 | } 484 | 485 | /***/ }), 486 | 487 | /***/ 45: 488 | /***/ (function(module, exports) { 489 | 490 | // removed by extract-text-webpack-plugin 491 | 492 | /***/ }), 493 | 494 | /***/ 46: 495 | /***/ (function(module, exports) { 496 | 497 | // removed by extract-text-webpack-plugin 498 | 499 | /***/ }) 500 | 501 | /******/ }); -------------------------------------------------------------------------------- /dist/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/post-form.js": "/js/post-form.js", 3 | "/js/tool.js": "/js/tool.js", 4 | "/css/tool.css": "/css/tool.css", 5 | "/css/post-form.css": "/css/post-form.css" 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "cross-env": "^5.0.0", 14 | "laravel-mix": "^1.0" 15 | }, 16 | "dependencies": { 17 | "vue": "^2.5.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/components/Tool.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 100 | -------------------------------------------------------------------------------- /resources/js/components/post-form/Create.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 203 | 204 | -------------------------------------------------------------------------------- /resources/js/components/post-form/DefaultField.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 55 | -------------------------------------------------------------------------------- /resources/js/components/post-form/Edit.vue: -------------------------------------------------------------------------------- 1 | 155 | 156 | 322 | -------------------------------------------------------------------------------- /resources/js/components/post-form/FieldSet.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 38 | -------------------------------------------------------------------------------- /resources/js/post-form.js: -------------------------------------------------------------------------------- 1 | Nova.booting((Vue, router) => { 2 | Vue.component('post-form-create', require('./components/post-form/Create')) 3 | Vue.component('post-form-edit', require('./components/post-form/Edit')) 4 | Vue.component('default-field', require('./components/post-form/DefaultField')) 5 | Vue.component('field-set', require('./components/post-form/FieldSet')) 6 | }) 7 | -------------------------------------------------------------------------------- /resources/js/tool.js: -------------------------------------------------------------------------------- 1 | Nova.booting((Vue, router) => { 2 | router.addRoutes([ 3 | { 4 | name: 'nova-blog-tool', 5 | path: '/nova-blog-tool', 6 | component: require('./components/Tool'), 7 | }, 8 | ]) 9 | }) 10 | -------------------------------------------------------------------------------- /resources/sass/post-form.scss: -------------------------------------------------------------------------------- 1 | // Nova Tool CSS 2 | -------------------------------------------------------------------------------- /resources/sass/tool.scss: -------------------------------------------------------------------------------- 1 | // Nova Tool CSS 2 | -------------------------------------------------------------------------------- /resources/views/navigation.blade.php: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Blog 12 | 13 |

14 | 61 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | published = true; 29 | $model->status = 'published'; 30 | $model->save(); 31 | } catch (Exception $e) { 32 | $this->markAsFailed($model, $e); 33 | return Action::danger('It not worked!'); 34 | } 35 | } 36 | return Action::message('Post Published!'); 37 | } 38 | 39 | /** 40 | * Get the fields available on the action. 41 | * 42 | * @return array 43 | */ 44 | public function fields() 45 | { 46 | return []; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Actions/SaveDraftPost.php: -------------------------------------------------------------------------------- 1 | published = false; 29 | $model->status = 'draft'; 30 | $model->save(); 31 | 32 | } catch (Exception $e) { 33 | $this->markAsFailed($model, $e); 34 | return Action::danger('It not worked!'); 35 | } 36 | } 37 | return Action::message('Post Saved!'); 38 | } 39 | 40 | /** 41 | * Get the fields available on the action. 42 | * 43 | * @return array 44 | */ 45 | public function fields() 46 | { 47 | return []; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/BlogResponder.php: -------------------------------------------------------------------------------- 1 | name = $name; 48 | $this->label = $label; 49 | $this->position = $position; 50 | $this->class = $class; 51 | $this->style = $style; 52 | parent::__construct($this->prepareFields($fields)); 53 | } 54 | 55 | /** 56 | * Prepare the panel for JSON serialization. 57 | * 58 | * @return array 59 | */ 60 | public function jsonSerialize() 61 | { 62 | return [ 63 | 'component' => $this->component, 64 | 'name' => $this->name, 65 | 'label' => $this->label, 66 | 'position' => $this->position, 67 | 'prefixComponent' => true, 68 | 'panel' => $this->panel, 69 | 'indexName' => $this->name, 70 | 'fields' => $this->data, 71 | 'class' => $this->class, 72 | 'style' => $this->style 73 | ]; 74 | } 75 | 76 | /** 77 | * Prepare the given fields. 78 | * 79 | * @param \Closure|array $fields 80 | * @return array 81 | */ 82 | protected function prepareFields($fields) 83 | { 84 | return collect(is_callable($fields) ? $fields() : $fields)->each(function ($field) { 85 | if($field instanceof Field) { 86 | $field->withMeta(['fieldSet' => $this->name]); 87 | } 88 | })->all(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Http/Controllers/BlogBaseController.php: -------------------------------------------------------------------------------- 1 | migrations = $migrator->getMigrations(); 32 | } 33 | 34 | /** 35 | * Undocumented function. 36 | * 37 | * @return \Illuminate\Http\JsonResponse 38 | */ 39 | public function execute() : JsonResponse 40 | { 41 | $this->processTask(); 42 | 43 | return response()->json(['messages' => $this->messages], 200); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Http/Controllers/DashboardController.php: -------------------------------------------------------------------------------- 1 | json([ 13 | 'installed' => Blog::isInstalled(), 14 | ], 200); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Http/Controllers/MigrationController.php: -------------------------------------------------------------------------------- 1 | migrations as $tableName => $migrationClass) { 13 | if (Schema::hasTable($tableName)) { 14 | $this->messages[] = BlogResponder::tableAlreadyCreated($tableName); 15 | } 16 | 17 | try { 18 | $migrationClass->up(); 19 | $this->messages[] = BlogResponder::migrationSuccess($tableName); 20 | } catch (\Exception $e) { 21 | $this->messages[] = BlogResponder::migrationFailure($tableName); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Controllers/ResetController.php: -------------------------------------------------------------------------------- 1 | migrations as $tableName => $migrationClass) { 14 | if (! Schema::hasTable($tableName)) { 15 | $this->messages[] = BlogResponder::resetTableNotFound($tableName); 16 | } 17 | 18 | try { 19 | $this->truncateTable($tableName); 20 | $this->messages[] = BlogResponder::resetSuccess($tableName); 21 | } catch (\Exception $e) { 22 | $this->messages[] = BlogResponder::resetFailure($tableName); 23 | } 24 | } 25 | } 26 | 27 | protected function truncateTable($tableName) 28 | { 29 | DB::statement('SET FOREIGN_KEY_CHECKS=0;'); 30 | DB::table($tableName)->truncate(); 31 | DB::statement('SET FOREIGN_KEY_CHECKS=1;'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Controllers/UninstallController.php: -------------------------------------------------------------------------------- 1 | migrations as $tableName => $migrationClass) { 14 | if (! Schema::hasTable($tableName)) { 15 | $this->messages[] = BlogResponder::deleteTableNotFound($tableName); 16 | } 17 | 18 | try { 19 | $this->deleteTable($migrationClass); 20 | $this->messages[] = BlogResponder::deleteSuccess($tableName); 21 | } catch (\Exception $e) { 22 | $this->messages[] = BlogResponder::deleteFailure($tableName); 23 | } 24 | } 25 | } 26 | 27 | protected function deleteTable($migrationClass) 28 | { 29 | DB::statement('SET FOREIGN_KEY_CHECKS=0;'); 30 | $migrationClass->down(); 31 | DB::statement('SET FOREIGN_KEY_CHECKS=1;'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Middleware/Authorize.php: -------------------------------------------------------------------------------- 1 | authorize($request) ? $next($request) : abort(403); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Metrics/Posts/NewPosts.php: -------------------------------------------------------------------------------- 1 | count($request, Post::class); 19 | } 20 | 21 | /** 22 | * Get the ranges available for the metric. 23 | * @return array 24 | */ 25 | public function ranges() 26 | { 27 | return [ 28 | 1 => 'Since Yesterday', 29 | 7 => 'Since Last Week', 30 | 30 => 'Since Last Month', 31 | 90 => 'Since 3 Months Ago', 32 | 180 => 'Since 6 Months Ago', 33 | 365 => 'Since Last Year', 34 | ]; 35 | } 36 | 37 | /** 38 | * Determine for how many minutes the metric should be cached. 39 | * @return \DateTimeInterface|\DateInterval|float|int 40 | */ 41 | public function cacheFor() 42 | { 43 | return now()->addMinutes(10); 44 | } 45 | 46 | /** 47 | * Get the URI key for the metric. 48 | * @return string 49 | */ 50 | public function uriKey() 51 | { 52 | return 'new-posts'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Metrics/Posts/PostsTrend.php: -------------------------------------------------------------------------------- 1 | countByDays($request, Post::class)->showLatestValue(); 19 | } 20 | 21 | /** 22 | * Get the ranges available for the metric. 23 | * @return array 24 | */ 25 | public function ranges() 26 | { 27 | return [ 28 | 1 => 'Today', 29 | 2 => 'Yesterday', 30 | 7 => 'Last 7 Days', 31 | 14 => 'Last 14 Days', 32 | 28 => 'Last 28 Days', 33 | 30 => 'Last 30 Days', 34 | 90 => 'Last 90 Days', 35 | 180 => 'Last 180 Days', 36 | 365 => 'Last Year', 37 | ]; 38 | } 39 | 40 | /** 41 | * Get the URI key for the metric. 42 | * @return string 43 | */ 44 | public function uriKey() 45 | { 46 | return 'posts-per-day'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Migrations/CategoryMigration.php: -------------------------------------------------------------------------------- 1 | increments('id'); 14 | $table->unsignedInteger('parent_id')->nullable(); 15 | $table->json('name'); 16 | $table->json('description')->nullable(); 17 | $table->string('slug')->unique(); 18 | $table->softDeletes(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('categories'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Migrations/CommentMigration.php: -------------------------------------------------------------------------------- 1 | increments('id'); 14 | $table->unsignedInteger('post_id'); 15 | $table->unsignedInteger('user_id'); 16 | $table->json('body'); 17 | $table->timestamps(); 18 | $table->softDeletes(); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::dropIfExists('comments'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Migrations/ForeignKeyMigration.php: -------------------------------------------------------------------------------- 1 | foreign('user_id')->references('id')->on('users'); 14 | }); 15 | 16 | Schema::table('comments', function (Blueprint $table) { 17 | $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade'); 18 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::table('posts', function (Blueprint $table) { 25 | $table->dropForeign(['user_id']); 26 | }); 27 | 28 | Schema::table('comments', function (Blueprint $table) { 29 | $table->dropForeign(['post_id']); 30 | $table->dropForeign(['user_id']); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Migrations/Migrator.php: -------------------------------------------------------------------------------- 1 | new CategoryMigration, 11 | 'posts' => new PostMigration, 12 | 'comments' => new CommentMigration, 13 | 'tags' => new TagMigration, 14 | 'post_category' => new PostCategoryPivotMigration, 15 | 'nova_pending_trix_attachments' => new NovaPendingTrixAttachmentsMigration(), 16 | 'nova_trix_attachments' => new NovaTrixAttachmentsMigration(), 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Migrations/NovaPendingTrixAttachmentsMigration.php: -------------------------------------------------------------------------------- 1 | increments('id'); 14 | $table->string('draft_id')->index(); 15 | $table->string('attachment'); 16 | $table->string('disk'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists('nova_pending_trix_attachments'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Migrations/NovaTrixAttachmentsMigration.php: -------------------------------------------------------------------------------- 1 | increments('id'); 14 | $table->string('attachable_type'); 15 | $table->unsignedInteger('attachable_id'); 16 | $table->string('attachment'); 17 | $table->string('disk'); 18 | $table->string('url')->index(); 19 | $table->timestamps(); 20 | 21 | $table->index(['attachable_type', 'attachable_id']); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('nova_trix_attachments'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Migrations/PostCategoryPivotMigration.php: -------------------------------------------------------------------------------- 1 | increments('id'); 14 | $table->unsignedInteger('post_id'); 15 | $table->unsignedInteger('category_id'); 16 | }); 17 | } 18 | 19 | public function down() 20 | { 21 | Schema::dropIfExists('post_category'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Migrations/PostMigration.php: -------------------------------------------------------------------------------- 1 | increments('id'); 14 | $table->unsignedInteger('parent_id')->nullable(); 15 | $table->json('title')->unique(); 16 | $table->json('content'); 17 | $table->json('category')->nullable(); 18 | $table->string('slug')->unique(); 19 | $table->unsignedInteger('menu_order')->default(0); 20 | 21 | $table->string('status')->default('draft'); 22 | $table->boolean('published')->default(false); 23 | $table->boolean('featured')->default(false); 24 | 25 | $table->timestamp('post_date')->useCurrent(); 26 | $table->timestamp('modified_date')->nullable(); 27 | $table->timestamp('publish_date')->nullable(); 28 | $table->timestamp('scheduled_for')->useCurrent(); 29 | 30 | $table->string('featured_image')->nullable(); 31 | $table->softDeletes(); 32 | $table->timestamps(); 33 | }); 34 | } 35 | 36 | public function down() 37 | { 38 | Schema::dropIfExists('posts'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Migrations/TagMigration.php: -------------------------------------------------------------------------------- 1 | increments('id'); 14 | $table->json('name'); 15 | $table->json('slug'); 16 | $table->string('type')->nullable(); 17 | $table->integer('order_column')->nullable(); 18 | $table->softDeletes(); 19 | $table->timestamps(); 20 | }); 21 | 22 | Schema::create('taggables', function (Blueprint $table) { 23 | $table->increments('id'); 24 | $table->integer('tag_id')->unsigned(); 25 | $table->integer('taggable_id')->unsigned(); 26 | $table->string('taggable_type'); 27 | 28 | $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); 29 | }); 30 | } 31 | 32 | public function down() 33 | { 34 | Schema::dropIfExists('tags'); 35 | Schema::dropIfExists('taggables'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Models/Category.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Post::class, 'post_category', 'post_id', 'category_id'); 34 | } 35 | 36 | /** 37 | * Get the route key for the model. 38 | * 39 | * @return string 40 | */ 41 | public function getRouteKeyName() 42 | { 43 | return 'slug'; 44 | } 45 | 46 | public function setAttribute($key, $value) 47 | { 48 | if (in_array($key, $this->translatable) && ! is_array($value)) { 49 | return $this->setTranslation($key, app()->getLocale(), $value); 50 | } 51 | return parent::setAttribute($key, $value); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Models/Comment.php: -------------------------------------------------------------------------------- 1 | 'integer', 33 | 'user_id' => 'integer', 34 | 'body' => 'string', 35 | ]; 36 | 37 | /** 38 | * The attributes that should be mutated to dates. 39 | * @var array 40 | */ 41 | protected $dates = [ 42 | 'created_at', 43 | 'updated_at', 44 | 'deleted_at', 45 | ]; 46 | 47 | /** 48 | * @return BelongsTo 49 | */ 50 | public function post(): BelongsTo 51 | { 52 | return $this->belongsTo(Post::class); 53 | } 54 | 55 | /** 56 | * @return BelongsTo 57 | */ 58 | public function author(): BelongsTo 59 | { 60 | return $this->belongsTo(config('nova-blog.user_model'), 'user_id'); 61 | } 62 | 63 | public function setAttribute($key, $value) 64 | { 65 | if (in_array($key, $this->translatable) && ! is_array($value)) { 66 | return $this->setTranslation($key, app()->getLocale(), $value); 67 | } 68 | return parent::setAttribute($key, $value); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Models/Image.php: -------------------------------------------------------------------------------- 1 | filename); 30 | } 31 | 32 | /** 33 | * Get image thumbnail's link. 34 | * @return string 35 | */ 36 | public function getThumbnailLinkAttribute() 37 | { 38 | return url('uploads/images/'.config('nova-blog.image_settings.disk').$this->thumbnail); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Models/Post.php: -------------------------------------------------------------------------------- 1 | 'boolean', 51 | 'scheduled_for' => 'datetime:Y-m-d H:i:s', 52 | ]; 53 | 54 | /** 55 | * The attributes that should be mutated to dates. 56 | * @var array 57 | */ 58 | protected $dates = [ 59 | 'scheduled_for', 60 | 'deleted_at', 61 | 'created_at', 62 | 'updated_at', 63 | ]; 64 | 65 | /** 66 | * Published mutator. 67 | * @return bool 68 | */ 69 | // public function getPublishedAttribute() 70 | // { 71 | // return now() > $this->scheduled_for; 72 | // } 73 | 74 | /** 75 | * The "booting" method of the model. 76 | * 77 | * @return void 78 | */ 79 | protected static function boot() 80 | { 81 | parent::boot(); 82 | 83 | self::updating(function($model){ 84 | // if($model->published) { 85 | // $model->status = 'published'; 86 | // } 87 | // if($model->status != 'published') { 88 | // $model->published = 0; 89 | // } 90 | }); 91 | 92 | self::creating(function($model){ 93 | if($model->published) { 94 | $model->status = 'published'; 95 | } 96 | }); 97 | 98 | self::created(function($model){ 99 | $model->category()->sync($model->queuedCategories, true); 100 | $model->queuedCategories = []; 101 | }); 102 | 103 | static::addGlobalScope(new PublishedScope()); 104 | } 105 | 106 | function setCategoryAttribute($value){ 107 | if (! $this->exists) { 108 | $this->queuedCategories = $value; 109 | return; 110 | } 111 | 112 | $this->category()->sync($value, true); 113 | } 114 | 115 | function getCategoryAttribute(){ 116 | return implode(',', $this->category()->pluck('category_id')->toArray()); 117 | } 118 | 119 | /** 120 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 121 | */ 122 | public function author(): BelongsTo 123 | { 124 | return $this->belongsTo(config('nova-blog.resources.users.model'), 'user_id'); 125 | } 126 | 127 | /** 128 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 129 | */ 130 | // public function featured_image(): BelongsTo 131 | // { 132 | // return $this->belongsTo(Image::class, 'image_id'); 133 | // } 134 | 135 | /** 136 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 137 | */ 138 | public function category(): BelongsToMany 139 | { 140 | return $this->belongsToMany(Category::class, 'post_category'); 141 | } 142 | 143 | /** 144 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 145 | */ 146 | public function comments(): HasMany 147 | { 148 | return $this->hasMany(Comment::class); 149 | } 150 | 151 | /** 152 | * Get the route key for the model. 153 | * 154 | * @return string 155 | */ 156 | public function getRouteKeyName() 157 | { 158 | return 'slug'; 159 | } 160 | 161 | public function setAttribute($key, $value) 162 | { 163 | if (in_array($key, $this->translatable) && ! is_array($value)) { 164 | return $this->setTranslation($key, app()->getLocale(), $value); 165 | } 166 | return parent::setAttribute($key, $value); 167 | } 168 | 169 | public function tags(): MorphToMany 170 | { 171 | return $this->morphToMany( 172 | Tag::class, 173 | 'taggable', 174 | 'taggables', 175 | 'taggable_id', 176 | 'tag_id' 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Models/Tag.php: -------------------------------------------------------------------------------- 1 | 'string', 30 | 'description' => 'string', 31 | 'tagged_count' => 'integer', 32 | ]; 33 | 34 | /** 35 | * The attributes that should be mutated to dates. 36 | * @var array 37 | */ 38 | protected $dates = [ 39 | 'created_at', 40 | 'updated_at', 41 | ]; 42 | 43 | /** 44 | * @return BelongsToMany 45 | */ 46 | 47 | public function posts(): MorphToMany 48 | { 49 | return $this->morphedByMany( 50 | Post::class, 51 | 'taggable', 52 | 'taggables', 53 | 'tag_id', 54 | 'taggable_id' 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/NovaBlogTool.php: -------------------------------------------------------------------------------- 1 | delete($image->filename); 18 | Storage::disk(config('nova-blog.image_settings.disk'))->delete(config('nova-blog.image_settings.path_thumb').$image->thumbnail); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Policies/CategoryPolicy.php: -------------------------------------------------------------------------------- 1 | can('view category'); 21 | } 22 | 23 | public function create(User $user) 24 | { 25 | return $user->can('create category'); 26 | } 27 | 28 | public function update(User $user, Category $category) 29 | { 30 | return $user->can('update category'); 31 | } 32 | 33 | public function delete(User $user, Category $category) 34 | { 35 | return $user->can('delete category'); 36 | } 37 | 38 | public function restore(User $user, Category $category) 39 | { 40 | return $user->can('restore category'); 41 | } 42 | 43 | public function forceDelete(User $user, Category $category) 44 | { 45 | return $user->can('force delete category'); 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Policies/CommentPolicy.php: -------------------------------------------------------------------------------- 1 | can('view comment'); 21 | } 22 | 23 | public function create(User $user) 24 | { 25 | return $user->can('create comment'); 26 | } 27 | 28 | public function update(User $user, Comment $comment) 29 | { 30 | return $user->can('update comment'); 31 | } 32 | 33 | public function delete(User $user, Comment $comment) 34 | { 35 | return $user->can('delete comment'); 36 | } 37 | 38 | public function restore(User $user, Comment $comment) 39 | { 40 | return $user->can('restore comment'); 41 | } 42 | 43 | public function forceDelete(User $user, Comment $comment) 44 | { 45 | return $user->can('force delete comment'); 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Policies/PostPolicy.php: -------------------------------------------------------------------------------- 1 | can('view post'); 21 | } 22 | 23 | public function create(User $user) 24 | { 25 | return $user->can('create post'); 26 | } 27 | 28 | public function update(User $user, Post $post) 29 | { 30 | return $user->can('update post'); 31 | } 32 | 33 | public function delete(User $user, Post $post) 34 | { 35 | return $user->can('delete post'); 36 | } 37 | 38 | public function restore(User $user, Post $post) 39 | { 40 | return $user->can('restore post'); 41 | } 42 | 43 | public function forceDelete(User $user, Post $post) 44 | { 45 | return $user->can('force delete post'); 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Policies/TagPolicy.php: -------------------------------------------------------------------------------- 1 | can('view tag'); 21 | } 22 | 23 | public function create(User $user) 24 | { 25 | return $user->can('create tag'); 26 | } 27 | 28 | public function update(User $user, Tag $tag) 29 | { 30 | return $user->can('update tag'); 31 | } 32 | 33 | public function delete(User $user, Tag $tag) 34 | { 35 | return $user->can('delete tag'); 36 | } 37 | 38 | public function restore(User $user, Tag $tag) 39 | { 40 | return $user->can('restore tag'); 41 | } 42 | 43 | public function forceDelete(User $user, Tag $tag) 44 | { 45 | return $user->can('force delete tag'); 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/PostForm.php: -------------------------------------------------------------------------------- 1 | featured_image; 15 | 16 | $file_extension = $image_file->getClientOriginalExtension(); 17 | 18 | $new_filename = str_random(8).'_'.time().''.str_random(32); 19 | 20 | $filename = $new_filename.'.'.$file_extension; 21 | 22 | $filename_thumb = $new_filename.'.'.$file_extension; 23 | 24 | Storage::disk(config('nova-blog.image_settings.disk')) 25 | ->put( 26 | config('nova-blog.image_settings.path').$filename, 27 | (string) file_get_contents($image_file) 28 | ); 29 | $image_thumb = ImageManagerStatic::make($image_file) 30 | ->resize( 31 | config('nova-blog.image_thumb_settings.width'), 32 | config('nova-blog.image_thumb_settings.height') 33 | )->save($filename_thumb); 34 | Storage::disk(config('nova-blog.image_settings.disk')) 35 | ->put( 36 | config('nova-blog.image_settings.path_thumb').$filename_thumb, 37 | $image_thumb 38 | ); 39 | 40 | //@todo Check is image uploaded with the post or alone in Image page. 41 | // $image_model = new Image; 42 | // $image_model->title = $image_file->getClientOriginalName(); 43 | // $image_model->filename = $filename; 44 | // $image_model->thumbnail = $filename_thumb; 45 | // $image_model->size = number_format( 46 | // $image_file->getSize() / 1000000, 47 | // 2 48 | // ).'MB'; 49 | // $image_model->save(); 50 | 51 | return [ 52 | //'image_id' => $image_model->id, 53 | 'featured_image' => $filename, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Resources/Category.php: -------------------------------------------------------------------------------- 1 | sortable(), 51 | Text::make('Name') 52 | ->rules(['required', 'string']) 53 | ->sortable(), 54 | Markdown::make('Description'), 55 | Multilingual::make('lang'), 56 | HasMany::make('Posts'), 57 | ]; 58 | } 59 | 60 | /** 61 | * Get the cards available for the request. 62 | * @param \Illuminate\Http\Request $request 63 | * @return array 64 | */ 65 | public function cards(Request $request) 66 | { 67 | return []; 68 | } 69 | 70 | /** 71 | * Get the filters available for the resource. 72 | * @param \Illuminate\Http\Request $request 73 | * @return array 74 | */ 75 | public function filters(Request $request) 76 | { 77 | return []; 78 | } 79 | 80 | /** 81 | * Get the lenses available for the resource. 82 | * @param \Illuminate\Http\Request $request 83 | * @return array 84 | */ 85 | public function lenses(Request $request) 86 | { 87 | return []; 88 | } 89 | 90 | /** 91 | * Get the actions available for the resource. 92 | * @param \Illuminate\Http\Request $request 93 | * @return array 94 | */ 95 | public function actions(Request $request) 96 | { 97 | return []; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Resources/Comment.php: -------------------------------------------------------------------------------- 1 | sortable(), 50 | 51 | BelongsTo::make('Post', 'post', Post::class) 52 | ->sortable() 53 | ->rules('required'), 54 | 55 | BelongsTo::make('Author', 'author', User::class) 56 | ->sortable() 57 | ->rules('required'), 58 | 59 | Markdown::make('Body') 60 | ->rules(['required', 'string']), 61 | ]; 62 | } 63 | 64 | /** 65 | * Get the cards available for the request. 66 | * @param \Illuminate\Http\Request $request 67 | * @return array 68 | */ 69 | public function cards(Request $request) 70 | { 71 | return []; 72 | } 73 | 74 | /** 75 | * Get the filters available for the resource. 76 | * @param \Illuminate\Http\Request $request 77 | * @return array 78 | */ 79 | public function filters(Request $request) 80 | { 81 | return []; 82 | } 83 | 84 | /** 85 | * Get the lenses available for the resource. 86 | * @param \Illuminate\Http\Request $request 87 | * @return array 88 | */ 89 | public function lenses(Request $request) 90 | { 91 | return []; 92 | } 93 | 94 | /** 95 | * Get the actions available for the resource. 96 | * @param \Illuminate\Http\Request $request 97 | * @return array 98 | */ 99 | public function actions(Request $request) 100 | { 101 | return []; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Resources/Post.php: -------------------------------------------------------------------------------- 1 | options([ 76 | 'draft' => 'Draft', 77 | 'published' => 'Published' 78 | ])->withMeta(['type' => 'hidden'])->onlyOnForms(), 79 | 80 | //Boolean::make('published')->onlyOnForms() 81 | ], 82 | 'main', '' 83 | ), 84 | 85 | $this->mainFieldSet(), 86 | 87 | new FieldSet( 'Tags', 88 | [ 89 | Tags::make('Tags')->withMeta(['label' => false]), 90 | ], 91 | 'side', 'Tags' 92 | ), 93 | new FieldSet( 'Category', 94 | [ 95 | Checkboxes::make('Category', 'category')->options(\DigitalCloud\NovaBlogTool\Models\Category::pluck('name', 'id')->toArray())->withMeta(['label' => false]), 96 | ], 97 | 'side', 'Category' 98 | ), 99 | new FieldSet( 'Featured Image', 100 | [ 101 | FilemanagerField::make('featured_image')->displayAsImage()->withMeta(['label' => false]) 102 | //ImageUpload::make('Image', 'featured_image', 'local')->store(new StoreImage), 103 | ], 104 | 'side', 'Featured Image' 105 | ), 106 | 107 | HasMany::make('Comments', 'comments', Comment::class) 108 | ->sortable() 109 | ->rules(['required']), 110 | ], $this->conditionalFields($request)); 111 | } 112 | 113 | /** 114 | * Get the cards available for the request. 115 | * @param \Illuminate\Http\Request $request 116 | * @return array 117 | */ 118 | public function cards(Request $request) 119 | { 120 | return [ 121 | (new NewPosts)->width('1/2'), 122 | (new PostsTrend)->width('1/2'), 123 | ]; 124 | } 125 | 126 | /** 127 | * Get the filters available for the resource. 128 | * @param \Illuminate\Http\Request $request 129 | * @return array 130 | */ 131 | public function filters(Request $request) 132 | { 133 | return []; 134 | } 135 | 136 | /** 137 | * Get the lenses available for the resource. 138 | * @param \Illuminate\Http\Request $request 139 | * @return array 140 | */ 141 | public function lenses(Request $request) 142 | { 143 | return []; 144 | } 145 | 146 | /** 147 | * Get the actions available for the resource. 148 | * @param \Illuminate\Http\Request $request 149 | * @return array 150 | */ 151 | public function actions(Request $request) 152 | { 153 | return [ 154 | new PublishPost(), 155 | new SaveDraftPost() 156 | ]; 157 | } 158 | 159 | public static function form ($request) { 160 | return new PostForm(); 161 | } 162 | 163 | private function mainFieldSet() { 164 | return new FieldSet( 'Main Info', 165 | [ 166 | ID::make()->sortable(), 167 | Text::make('title')->rules('required') 168 | ->withMeta(['extraAttributes' => ['class' => 'attr1val', 'fullWidthContent' => true]]) 169 | ->withMeta(['showLabel' => true]), 170 | Select::make('status')->options([ 171 | 'draft' => 'Draft', 172 | 'published' => 'Published' 173 | ])->withMeta(['type' => 'hidden'])->exceptOnForms(), 174 | Boolean::make('Published')->withMeta(['type' => 'hidden']), 175 | 176 | Trix::make('content')->withFiles('public')->rules('required')->withMeta(['label' => false]), 177 | Multilingual::make('lang'), 178 | ], 179 | 'main', 'Main Info' 180 | ); 181 | } 182 | 183 | private function conditionalFields(Request $request) { 184 | return ToolServiceProvider::availableAdditionalFields($request); 185 | // $fields = []; 186 | // if(in_array('DigitalCloud\ServiceTool\Resources\Service', Nova::availableResources($request))) { 187 | // $fields[] = BelongsToMany::make('Services', 'service', \DigitalCloud\ServiceTool\Resources\Service::class); 188 | // } 189 | // return $fields; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Resources/Resource.php: -------------------------------------------------------------------------------- 1 | isMethod('get')) { 71 | return $creationFields; 72 | } 73 | $indexedCreationFields = $creationFields->mapWithKeys(function ($item) { 74 | return [$item->attribute => $item]; 75 | }); 76 | 77 | return $this->availableFieldSet($request, $indexedCreationFields); 78 | } 79 | 80 | public function updateFields(NovaRequest $request) { 81 | $updateFields = parent::updateFields($request); 82 | if(!$request->isMethod('get')) { 83 | return $updateFields; 84 | } 85 | $indexedUpdateFields = $updateFields->mapWithKeys(function ($item) { 86 | return [$item->attribute => $item]; 87 | }); 88 | return $this->availableFieldSet($request, $indexedUpdateFields); 89 | } 90 | 91 | public function detailFields(NovaRequest $request) 92 | { 93 | $detailFields = parent::detailFields($request); 94 | if(!$request->isMethod('get')) { 95 | return $detailFields; 96 | } 97 | //$detailFields = $this->availableFieldSet($request, $detailFields); 98 | return $detailFields; 99 | 100 | } 101 | 102 | public function availableFieldSet(NovaRequest $request, $fields) 103 | { 104 | $fieldSets = collect(array_values($this->fields($request))) 105 | ->whereInstanceOf(FieldSet::class)->mapWithKeys(function ($fieldSet) { 106 | return [camel_case($fieldSet->name) => $fieldSet]; 107 | }); 108 | 109 | if(count($fieldSets) < 1) { 110 | return $fields; 111 | } 112 | $this->assignFieldsToFieldSet($request, $fields); 113 | 114 | foreach ($fieldSets as $fieldSet) { 115 | $setfields = $fields->filter(function ($field) use ($fieldSet){ 116 | return $field->meta['fieldSet'] == $fieldSet->name; 117 | }); 118 | $fieldSet->data = $setfields; 119 | } 120 | return $fieldSets; 121 | } 122 | 123 | protected function assignFieldsToFieldSet(NovaRequest $request, $fields) 124 | { 125 | foreach ($fields as $field) { 126 | $name = $field->meta['fieldSet'] ?? Panel::defaultNameFor($request->newResource()); 127 | $field->meta['fieldSet'] = $name; 128 | } 129 | return $fields; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Resources/Tag.php: -------------------------------------------------------------------------------- 1 | getLocale(); 52 | return [ 53 | ID::make()->sortable(), 54 | 55 | Text::make('name') 56 | ->sortable() 57 | ->rules(['required', 'string', 'max:255']) 58 | ->creationRules(['unique:tags,name->' . $locale]) 59 | ->updateRules(['unique:tags,name->' . $locale . ',{{resourceId}}']), 60 | Multilingual::make('lang'), 61 | MorphToMany::make('Posts', 'posts', Post::class), 62 | ]; 63 | } 64 | 65 | /** 66 | * Get the cards available for the request. 67 | * @param \Illuminate\Http\Request $request 68 | * @return array 69 | */ 70 | public function cards(Request $request) 71 | { 72 | return []; 73 | } 74 | 75 | /** 76 | * Get the filters available for the resource. 77 | * @param \Illuminate\Http\Request $request 78 | * @return array 79 | */ 80 | public function filters(Request $request) 81 | { 82 | return []; 83 | } 84 | 85 | /** 86 | * Get the lenses available for the resource. 87 | * @param \Illuminate\Http\Request $request 88 | * @return array 89 | */ 90 | public function lenses(Request $request) 91 | { 92 | return []; 93 | } 94 | 95 | /** 96 | * Get the actions available for the resource. 97 | * @param \Illuminate\Http\Request $request 98 | * @return array 99 | */ 100 | public function actions(Request $request) 101 | { 102 | return []; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Scopes/PublishedScope.php: -------------------------------------------------------------------------------- 1 | where('scheduled_for', '<=', date('Y-m-d H:i:s')); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ToolServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/../resources/views', 'nova-blog-tool'); 28 | 29 | $this->app->booted(function () { 30 | $this->routes(); 31 | 32 | Blog::injectToolResources(); 33 | }); 34 | 35 | $this->publishes([ 36 | $this->configPath() => config_path('nova-blog.php'), 37 | ], 'nova-blog-config'); 38 | } 39 | 40 | /** 41 | * Register the tool's routes. 42 | * 43 | * @return void 44 | */ 45 | protected function routes() 46 | { 47 | if ($this->app->routesAreCached()) { 48 | return; 49 | } 50 | 51 | Route::middleware(['nova', Authorize::class]) 52 | ->prefix('nova-vendor/nova-blog-tool') 53 | ->group(__DIR__.'/../routes/api.php'); 54 | } 55 | 56 | /** 57 | * Register any application services. 58 | * 59 | * @return void 60 | */ 61 | public function register() 62 | { 63 | $this->mergeConfigFrom($this->configPath(), 'nova-blog'); 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | protected function configPath() 70 | { 71 | return __DIR__.'/../config/nova-blog.php'; 72 | } 73 | 74 | /** 75 | * Register the given additional fields. 76 | * 77 | * @param array $additionalFields 78 | * @return void 79 | */ 80 | public static function additionalFields(array $additionalFields) 81 | { 82 | static::$additionalFields = array_merge(static::$additionalFields, $additionalFields); 83 | } 84 | 85 | /** 86 | * Get registered additional fields . 87 | * 88 | * @param \Illuminate\Http\Request $request 89 | * @return mixed 90 | */ 91 | public static function availableAdditionalFields(Request $request) 92 | { 93 | return collect(static::$additionalFields) 94 | ->filter 95 | ->authorize($request) 96 | ->all(); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/Traits/Sluggable.php: -------------------------------------------------------------------------------- 1 | slug = static::generateUniqueSlug( 27 | $model->{$column}, 28 | $event === 'updating' 29 | ? $model->id 30 | : null 31 | ); 32 | }); 33 | } 34 | } 35 | 36 | /** 37 | * Fetch sluggable field. 38 | * @param Model $model 39 | * @return string|null 40 | */ 41 | protected static function getSluggableField(Model $model): ?string 42 | { 43 | $table = $model->getTable(); 44 | 45 | switch ($table) { 46 | case 'posts': 47 | return 'title'; 48 | case 'categories': 49 | return 'name'; 50 | default: 51 | return null; 52 | } 53 | } 54 | 55 | /** 56 | * Generate a unique slug. 57 | * @param string $fieldValue 58 | * @param int|null $oldId 59 | * @return string 60 | */ 61 | protected static function generateUniqueSlug(string $fieldValue, int $oldId = null): string 62 | { 63 | $slug = str_slug($fieldValue); 64 | 65 | $additionalQuery = static::getAdditionalQueryString($oldId); 66 | 67 | $latestSlug = static::whereRaw("(slug = '$slug' or slug LIKE '$slug-%'){$additionalQuery}") 68 | ->latest('id') 69 | ->value('slug'); 70 | 71 | if ($latestSlug) { 72 | $pieces = explode('-', $latestSlug); 73 | 74 | $slug .= '-'.(intval(end($pieces)) + 1); 75 | } 76 | 77 | return $slug; 78 | } 79 | 80 | /** 81 | * Additional check for "updating" event. 82 | * Solves a problem when updating without changing sluggable field. 83 | * @param int|null $oldId 84 | * @return string 85 | */ 86 | protected static function getAdditionalQueryString($oldId): string 87 | { 88 | if (is_null($oldId)) { 89 | return ''; 90 | } 91 | 92 | return " AND id != '$oldId'"; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | 3 | mix.setPublicPath('dist') 4 | .js('resources/js/tool.js', 'js') 5 | .sass('resources/sass/tool.scss', 'css') 6 | 7 | mix.js('resources/js/post-form.js', 'js') 8 | .sass('resources/sass/post-form.scss', 'css') 9 | .webpackConfig({ 10 | resolve: { 11 | alias: { 12 | '@nova': path.resolve(__dirname, '../../vendor/laravel/nova/resources/js/') 13 | }, 14 | symlinks: false 15 | } 16 | }) 17 | --------------------------------------------------------------------------------