diff --git a/.env.example b/.env.example index 2f720f5..dee110b 100644 --- a/.env.example +++ b/.env.example @@ -39,16 +39,20 @@ YOUDAO_APP_SECRET= GOOGLE_ANALYTICS_ID= +ENABLE_VISITOR_LOG=false + COMMENT_DRIVER= DISQUS_SHORT_NAME= -FILESYSTEM_DRIVER= +FILESYSTEM_DRIVER=public ADMIN_EMAIL= MAIL_FROM_ADDRESS= MAIL_FROM_NAME= -SCOUT_QUEUE=true +SCOUT_QUEUE=false +SCOUT_DRIVER=null + ALGOLIA_APP_ID= ALGOLIA_SECRET= diff --git a/CHANGELOG.md b/CHANGELOG.md index 9da6734..c4a63e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +## 1.0.4 - 2018-03-19 +### Added +- [README.md](README.md) +- License + +### Changed +- Update composer packages +- Latest Google Analytics js code +- Default configurations + +### Fixed +- Fix error while versioning copied files ([82ba259](https://github.com/MilesPong/indigo/commit/82ba25917cd87e1754fdc7309b2c7ae3d90d6995)) +- Fix shedule bug +- Fix search bar height in chrome + ## 1.0.0 - 2018-03-12 ### Added - Package spatie/laravel-backup and related schedule diff --git a/README.md b/README.md index 513b979..573e616 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ - # Indigo -A blog built with [Laravel](https://laravel.com) and [Materialize](http://materializecss.com). +A blog application built with [Laravel](https://laravel.com), [Materialize](http://materializecss.com) and [Vue.js](https://vuejs.org/). + +Indigo is a project I mainly learn to how to develop a Laravel application **in the right way**, include using design patterns, modern coding tricks, and other useful skills. + +There is an introduction(Chinese) about this project as well, you can access it [here](https://immiles.com/articles/indigo). + +## Screenshot + +![screenshot](https://user-images.githubusercontent.com/5867628/37555740-48334dc4-2a27-11e8-973f-f54f96d9e912.png) ## Features - - Base blog features like post, page, archives, search, etc. + - Basic blog features like post, page, archives, search, etc. - Material UI (responsive layout) - Disqus comment integrated - Repositories pattern @@ -13,15 +20,15 @@ A blog built with [Laravel](https://laravel.com) and [Materialize](http://materi - Trash support - Counter with multiple drivers support - Backup with Google Drive storage support - - Multiple mix for building backend and frontend + - Multiple Mix for compiling backend and frontend -More features can be discovery in [CHANGELOG.md](CHANGELOG.md) +More features can be found in [CHANGELOG.md](CHANGELOG.md) ## Server Requirements -Basic requirements are listed in official [document](https://laravel.com/docs/5.5#server-requirements). +Basic requirements are listed in the official [document](https://laravel.com/docs/5.5#server-requirements). -Also additional services below may be used and **recommended** +Also, additional services below may be used and **recommended** - Redis - Algolia @@ -35,10 +42,51 @@ Also additional services below may be used and **recommended** ```bash $ git clone https://github.com/MilesPong/indigo $ cd indigo +$ composer install $ php artisan key:generate -$ cp .env.example .env # Change your DB settings and other services' config +$ cp .env.example .env +``` + +Change your DB settings and other services' configurations + +``` +# For Chinese translation in slug +YOUDAO_APP_KEY= +YOUDAO_APP_SECRET= + +# Google Analytics +GOOGLE_ANALYTICS_ID= + +# Visitor log +ENABLE_VISITOR_LOG=false + +# Comment +COMMENT_DRIVER= +DISQUS_SHORT_NAME= + +FILESYSTEM_DRIVER=public + +# For receiving feedback while failed in backup +ADMIN_EMAIL= + +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME= + +# Search +SCOUT_QUEUE=false +SCOUT_DRIVER=null +``` + +**Schedule** are required by default, set it up as follow + +```bash +$ crontab -e +# Append this to the end +# * * * * * php /path/to/project/artisan schedule:run >> /dev/null 2>&1 ``` +**Auto backup** is enabled by default, you may have a look about the configuration under [config/backup.php](config/backup.php) + ### Migration *Default user(admin) info is in [InitializationSeeder](database/seeds/InitializationSeeder.php ), you can modify it before running the seed task.* @@ -47,15 +95,23 @@ $ cp .env.example .env # Change your DB settings and other services' config $ php artisan migrate --seed # Migration and seeding ``` -### Others +### Compiling Assets -You may also set up the [schedule](https://laravel.com/docs/5.5/scheduling) and [queue](https://laravel.com/docs/5.5/queues) according to official docs to enable **counter** and **auto backup**. (See [commands](app/Console/Kernel.php)) +```bash +$ npm install +$ npm run dev # Frontend +$ npm run admin-dev # Backend +``` **Note: Code is open-sourced and you know what to do when "Something went wrong".** ## Changelog -Refer to the [Changelog](CHANGELOG.md) for a full history of the project. +Refer to the [CHANGELOG.md](CHANGELOG.md) for a full history of the project. + +## TODO + +Check this out in [Gist](https://gist.github.com/MilesPong/7529f9586fb7070a7f4c56360cdf9475). ## Links @@ -63,12 +119,10 @@ Refer to the [Changelog](CHANGELOG.md) for a full history of the project. - [Vuejs](https://vuejs.org) - [Laravel](https://laravel.com) -## About Indigo - -Indigo is a project I mainly learn to how to develop a Laravel application **in the right way**, include using design patterns, modern coding tricks and other useful skills. +## Contributing -There is a full introduction about this project as well, you can access it [here](https://immiles.com/articles/indigo). +Any bug report or pull request is welcome. ## License -The project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). +The project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). \ No newline at end of file diff --git a/app/Http/Requests/StoreUpdateCategoryRequest.php b/app/Http/Requests/StoreUpdateCategoryRequest.php index 018cdb5..6d1f247 100644 --- a/app/Http/Requests/StoreUpdateCategoryRequest.php +++ b/app/Http/Requests/StoreUpdateCategoryRequest.php @@ -26,7 +26,7 @@ public function rules() { $rules = [ 'name' => 'required|min:2|max:255|unique:categories', - 'slug' => 'unique:categories' + 'slug' => 'required|unique:categories' ]; switch ($this->method()) { @@ -40,6 +40,7 @@ public function rules() Rule::unique('categories')->ignore($this->route('category')) ], 'slug' => [ + 'required', Rule::unique('categories')->ignore($this->route('category')) ] ]); diff --git a/app/Http/Requests/StoreUpdatePageRequest.php b/app/Http/Requests/StoreUpdatePageRequest.php index f681f32..b5cf79a 100644 --- a/app/Http/Requests/StoreUpdatePageRequest.php +++ b/app/Http/Requests/StoreUpdatePageRequest.php @@ -33,7 +33,7 @@ public function rules() { $rules = [ 'title' => 'required', - 'slug' => 'unique:pages', + 'slug' => 'required|unique:pages', 'body' => 'required', ]; @@ -42,6 +42,7 @@ public function rules() case 'PATCH': $rules = array_merge($rules, [ 'slug' => [ + 'required', Rule::unique('pages')->ignore($this->route('page')) ] ]); diff --git a/app/Http/Requests/StoreUpdatePostRequest.php b/app/Http/Requests/StoreUpdatePostRequest.php index cef77ce..011cf76 100644 --- a/app/Http/Requests/StoreUpdatePostRequest.php +++ b/app/Http/Requests/StoreUpdatePostRequest.php @@ -31,7 +31,7 @@ public function rules() 'title' => 'required', 'description' => 'required|max:100', 'category_id' => 'required|exists:categories,id', - 'slug' => 'unique:posts', + 'slug' => 'required|unique:posts', 'body' => 'required', 'feature_img' => 'required', ]; @@ -41,6 +41,7 @@ public function rules() case "PATCH": $rules = array_merge($rules, [ 'slug' => [ + 'required', Rule::unique('posts')->ignore($this->route('post')) ] ]); diff --git a/app/Http/Requests/StoreUpdateTagRequest.php b/app/Http/Requests/StoreUpdateTagRequest.php index 53f4755..802bfd4 100644 --- a/app/Http/Requests/StoreUpdateTagRequest.php +++ b/app/Http/Requests/StoreUpdateTagRequest.php @@ -27,7 +27,7 @@ public function rules() $rules = [ 'name' => 'required|min:2|max:255|unique:tags', 'description' => 'max:255', - 'slug' => 'unique:tags' + 'slug' => 'required|unique:tags' ]; switch ($this->method()) { @@ -42,6 +42,7 @@ public function rules() ], 'description' => 'max:255', 'slug' => [ + 'required', Rule::unique('tags')->ignore($this->route('tag')) ] ]; diff --git a/app/Http/ViewComposers/GoogleAnalyticsComposer.php b/app/Http/ViewComposers/GoogleAnalyticsComposer.php new file mode 100644 index 0000000..b278221 --- /dev/null +++ b/app/Http/ViewComposers/GoogleAnalyticsComposer.php @@ -0,0 +1,20 @@ +with('traceId', config('indigo.analytics.google_trace_id')); + } +} \ No newline at end of file diff --git a/app/Providers/ComposerServiceProvider.php b/app/Providers/ComposerServiceProvider.php index eb9b2f1..d8380d4 100644 --- a/app/Providers/ComposerServiceProvider.php +++ b/app/Providers/ComposerServiceProvider.php @@ -4,6 +4,7 @@ use App\Http\ViewComposers\CategoriesComposer; use App\Http\ViewComposers\CommentComposer; +use App\Http\ViewComposers\GoogleAnalyticsComposer; use App\Http\ViewComposers\HotPostsComposer; use App\Http\ViewComposers\TagsComposer; use Illuminate\Support\Facades\View; @@ -26,6 +27,7 @@ public function boot() View::composer('widgets.tag', TagsComposer::class); View::composer('widgets.hot', HotPostsComposer::class); View::composer('partials.comment', CommentComposer::class); + View::composer('partials.google_analytics', GoogleAnalyticsComposer::class); } /** diff --git a/app/Repositories/Eloquent/CategoryRepositoryEloquent.php b/app/Repositories/Eloquent/CategoryRepositoryEloquent.php index 4f7c4cd..cfc7d6c 100644 --- a/app/Repositories/Eloquent/CategoryRepositoryEloquent.php +++ b/app/Repositories/Eloquent/CategoryRepositoryEloquent.php @@ -50,8 +50,6 @@ public function create(array $attributes) */ protected function preHandleData(array $attributes) { - $attributes['slug'] = $this->autoSlug($attributes['slug'], $attributes['name']); - return $attributes; } diff --git a/app/Repositories/Eloquent/PageRepositoryEloquent.php b/app/Repositories/Eloquent/PageRepositoryEloquent.php index c158288..01fba98 100644 --- a/app/Repositories/Eloquent/PageRepositoryEloquent.php +++ b/app/Repositories/Eloquent/PageRepositoryEloquent.php @@ -90,8 +90,6 @@ public function create(array $attributes) */ protected function preHandleData(array $attributes) { - $attributes['slug'] = $this->autoSlug($attributes['slug'], $attributes['title']); - return $this->handle($attributes); } diff --git a/app/Repositories/Eloquent/PostRepositoryEloquent.php b/app/Repositories/Eloquent/PostRepositoryEloquent.php index 0404505..1ea330e 100644 --- a/app/Repositories/Eloquent/PostRepositoryEloquent.php +++ b/app/Repositories/Eloquent/PostRepositoryEloquent.php @@ -118,8 +118,6 @@ public function create(array $attributes) */ protected function preHandleData(array $attributes) { - $attributes['slug'] = $this->autoSlug($attributes['slug'], $attributes['title']); - return $this->handle($attributes); } diff --git a/app/Repositories/Eloquent/TagRepositoryEloquent.php b/app/Repositories/Eloquent/TagRepositoryEloquent.php index 3d01afe..bf17152 100644 --- a/app/Repositories/Eloquent/TagRepositoryEloquent.php +++ b/app/Repositories/Eloquent/TagRepositoryEloquent.php @@ -50,8 +50,6 @@ public function create(array $attributes) */ protected function preHandleData(array $attributes) { - $attributes['slug'] = $this->autoSlug($attributes['slug'], $attributes['name']); - return $attributes; } diff --git a/app/Repositories/Eloquent/Traits/Slugable.php b/app/Repositories/Eloquent/Traits/Slugable.php index ffd468b..8a6e184 100644 --- a/app/Repositories/Eloquent/Traits/Slugable.php +++ b/app/Repositories/Eloquent/Traits/Slugable.php @@ -36,55 +36,6 @@ protected function ifShouldIgnorePublishedStatus() } } - /** - * Auto create slug if request slug is null - * - * @param $slug - * @param $toBeTranslated - * @return \JellyBool\Translug\Translation|mixed|string - */ - public function autoSlug($slug, $toBeTranslated) - { - if (!empty($slug)) { - return $slug; - } - - $slug = str_slug_with_cn($toBeTranslated); - - if ($this->slugExists($slug)) { - $slug = $this->addUniqueChar($slug); - } - - return $slug; - } - - /** - * @param $slug - * @param string $column - * @return mixed - */ - protected function slugExists($slug, $column = 'slug') - { - return $this->model->where($column, $slug)->exists(); - } - - /** - * @param $slug - * @return string - */ - protected function addUniqueChar($slug) - { - return $slug . '-' . $this->uniqueChar(); - } - - /** - * @return string - */ - protected function uniqueChar() - { - return Carbon::now()->timestamp; - } - /** * @param $id * @return string diff --git a/app/helpers.php b/app/helpers.php index 353b59b..40ff422 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -64,7 +64,7 @@ function str_slug_with_cn($text, $forceTrans = false) return ''; } - if ($forceTrans || hasChinese($text)) { + if (($forceTrans || hasChinese($text)) && hasSetYouDao()) { return translug($text); } @@ -72,6 +72,16 @@ function str_slug_with_cn($text, $forceTrans = false) } } +if (!function_exists('hasSetYouDao')) { + /** + * @return bool + */ + function hasSetYouDao() + { + return config('services.youdao.appKey') && config('services.youdao.appSecret'); + } +} + if (!function_exists('isAdmin')) { /** * Determine if current user is login and is admin. @@ -114,7 +124,8 @@ function random_img_url($width = 640, $height = 480) * @param int $decimals * @return string */ - function human_filesize($bytes, $decimals = 2) { + function human_filesize($bytes, $decimals = 2) + { $sz = 'BKMGTP'; $factor = floor((strlen($bytes) - 1) / 3); return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; diff --git a/resources/assets/admin/js/editor.js b/resources/assets/admin/js/editor.js new file mode 100644 index 0000000..abbd208 --- /dev/null +++ b/resources/assets/admin/js/editor.js @@ -0,0 +1,93 @@ +$(function () { + $('#description').trigger('autoresize'); + + /* + * TODO + * 1. use axios + * 2. result feedback + * */ + $('#title').blur(e => { + let text = e.target.value; + if (!text || $('#slug').val()) { + return; + } + $.post($slugTranslationURL, {text}) + .done(data => { + $('#slug').val(data.slug); + $('#slug-label').addClass('active'); + }) + .fail((jqXHR, textStatus, errorThrown) => { + // Log the error to the console + console.error( + "The following error occurred: " + + textStatus, errorThrown + ); + }) + }); + + /* + * TODO + * 1. use axios + * 2. uploading feedback + * 3. result feedback + * */ + $('#file-upload').change(e => { + let fd = new FormData(); + fd.append('image', e.target.files[0]); + + $.ajax({ + url: $imageUploadURL, + type: 'POST', + data: fd, + processData: false, + contentType: false + }).done(data => { + console.log(data); + $('#feature_img').val(data.path); + }).fail((jqXHR, textStatus, errorThrown) => { + // Log the error to the console + console.error( + "The following error occurred: " + + textStatus, errorThrown + ); + swal( + 'Oops...', + 'Something went wrong!', + 'error' + ) + }) + }); + + $('#body-label').addClass('active'); + + $('#published_at').click(() => { + dateInstance.open() + }); + + $('#published_time').change((e) => { + $('#published_at').val($('#published_date').val() + ' ' + e.target.value); + $('#published_at-label').addClass('active'); + }); +}); + +let simplemde = new SimpleMDE({ + element: document.getElementById("body"), + spellChecker: false // Multiple languages haven't been supported yet +}); + +// TODO temporarily fixed empty form body string when first submit +simplemde.codemirror.on("change", function () { + $('#body').val(simplemde.value()); +}); + +let timeInstance = M.Timepicker.init(document.querySelector('.timepicker')); + +let dateInstance = M.Datepicker.init(document.querySelector('.datepicker'), { + format: 'yyyy-mm-dd', + onClose: () => { + if ($('#published_date').val()) { + // TODO will-change memory exhausted? + timeInstance.open(); + } + } +}); \ No newline at end of file diff --git a/resources/assets/admin/sass/editor.scss b/resources/assets/admin/sass/editor.scss new file mode 100644 index 0000000..4ca0493 --- /dev/null +++ b/resources/assets/admin/sass/editor.scss @@ -0,0 +1,68 @@ +@media only screen and (min-width: 993px) { + .body-field .CodeMirror-fullscreen, .body-field .fullscreen { + margin-left: 300px; + } +} + +.file-path { + display: none; +} + +.editor-toolbar { + margin-top: 15px; +} + +.body-field { + + .CodeMirror-fullscreen { + top: 110px; + } + + .fullscreen { + margin-top: 64px; + } + + .editor-preview, .editor-preview-side { + line-height: 2.0; + font-size: 16px; + + strong { + font-weight: 600; + } + + h1, h2, h3, h4, h5, h6 { + font-weight: 600; + } + + h1 { + font-size: 2.32rem !important; + } + h2 { + font-size: 1.78rem !important; + } + h3 { + font-size: 1.60rem !important; + } + h4 { + font-size: 1.35rem !important; + } + h5 { + font-size: 1.20rem !important; + } + h6 { + font-size: 1.15rem !important; + } + + ul { + margin-left: 25px; + + li { + list-style-type: initial; + } + } + + img { + max-width: 100%; + } + } +} \ No newline at end of file diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 245d9f6..6bd6554 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -58,8 +58,4 @@ code:not([class]) { hyphens: none; position: relative; font-size: 1.1rem; -} - -strong { - font-weight: 600; } \ No newline at end of file diff --git a/resources/assets/sass/modules/article.scss b/resources/assets/sass/modules/article.scss index 23abe5a..676c042 100644 --- a/resources/assets/sass/modules/article.scss +++ b/resources/assets/sass/modules/article.scss @@ -45,10 +45,10 @@ .post-main { padding-top: 1rem; font-size: 16px; - line-height: 1.8; + line-height: 2.0; - h1,h2,h3,h4,h5,h6 { - font-weight: 600; + h1,h2,h3,h4,h5,h6,strong { + font-weight: bold; } h2 { @@ -75,6 +75,11 @@ .card-panel { padding-bottom: 0; + @media #{$small-and-down} { + padding-left: 14px; + padding-right: 14px; + } + &.page-panel { padding-bottom: 24px !important; } diff --git a/resources/views/admin/pages/_form.blade.php b/resources/views/admin/pages/_form.blade.php index 7bb2aff..0c3f28f 100644 --- a/resources/views/admin/pages/_form.blade.php +++ b/resources/views/admin/pages/_form.blade.php @@ -9,7 +9,7 @@