Просмотр исходного кода

Merge branch 'dev' into dev-rewardPoints

bianjunhui 13 часов назад
Родитель
Сommit
4b00af26ec
88 измененных файлов с 12531 добавлено и 12 удалено
  1. 6 6
      bootstrap/cache/services.php
  2. 1 0
      bootstrap/providers.php
  3. 1 0
      composer.json
  4. 101 0
      packages/Longyi/Blog/README.md
  5. 30 0
      packages/Longyi/Blog/composer.json
  6. 17 0
      packages/Longyi/Blog/package.json
  7. 96 0
      packages/Longyi/Blog/src/Config/acl.php
  8. 53 0
      packages/Longyi/Blog/src/Config/menu.php
  9. 7 0
      packages/Longyi/Blog/src/Contracts/Blog.php
  10. 7 0
      packages/Longyi/Blog/src/Contracts/Category.php
  11. 7 0
      packages/Longyi/Blog/src/Contracts/Comment.php
  12. 7 0
      packages/Longyi/Blog/src/Contracts/Tag.php
  13. 50 0
      packages/Longyi/Blog/src/Database/Migrations/2023_03_21_172616_create_blogs_table.php
  14. 42 0
      packages/Longyi/Blog/src/Database/Migrations/2023_03_21_175157_create_blog_categories_table.php
  15. 41 0
      packages/Longyi/Blog/src/Database/Migrations/2023_03_21_175231_create_blog_tags_table.php
  16. 39 0
      packages/Longyi/Blog/src/Database/Migrations/2023_03_21_175251_create_blog_comments_table.php
  17. 32 0
      packages/Longyi/Blog/src/Database/Migrations/2024_01_01_000000_add_m_src_to_blogs_table.php
  18. 239 0
      packages/Longyi/Blog/src/Datagrids/BlogDataGrid.php
  19. 151 0
      packages/Longyi/Blog/src/Datagrids/CategoryDataGrid.php
  20. 151 0
      packages/Longyi/Blog/src/Datagrids/CommentDataGrid.php
  21. 134 0
      packages/Longyi/Blog/src/Datagrids/TagDataGrid.php
  22. 91 0
      packages/Longyi/Blog/src/Export/BlogExport.php
  23. 289 0
      packages/Longyi/Blog/src/Http/Controllers/Admin/BlogController.php
  24. 214 0
      packages/Longyi/Blog/src/Http/Controllers/Admin/CategoryController.php
  25. 190 0
      packages/Longyi/Blog/src/Http/Controllers/Admin/CommentController.php
  26. 88 0
      packages/Longyi/Blog/src/Http/Controllers/Admin/ImportController.php
  27. 154 0
      packages/Longyi/Blog/src/Http/Controllers/Admin/SettingController.php
  28. 193 0
      packages/Longyi/Blog/src/Http/Controllers/Admin/TagController.php
  29. 420 0
      packages/Longyi/Blog/src/Http/Controllers/Shop/API/Blogs/BlogController.php
  30. 71 0
      packages/Longyi/Blog/src/Http/Controllers/Shop/ArticleController.php
  31. 221 0
      packages/Longyi/Blog/src/Http/Controllers/Shop/BlogController.php
  32. 83 0
      packages/Longyi/Blog/src/Http/Controllers/Shop/CategoryController.php
  33. 87 0
      packages/Longyi/Blog/src/Http/Controllers/Shop/CommentController.php
  34. 77 0
      packages/Longyi/Blog/src/Http/Controllers/Shop/TagController.php
  35. 52 0
      packages/Longyi/Blog/src/Http/Requests/BlogCategoryRequest.php
  36. 64 0
      packages/Longyi/Blog/src/Http/Requests/BlogRequest.php
  37. 52 0
      packages/Longyi/Blog/src/Http/Requests/BlogTagRequest.php
  38. 322 0
      packages/Longyi/Blog/src/Import/BlogImport.php
  39. 102 0
      packages/Longyi/Blog/src/Models/Blog.php
  40. 138 0
      packages/Longyi/Blog/src/Models/Category.php
  41. 42 0
      packages/Longyi/Blog/src/Models/Comment.php
  42. 26 0
      packages/Longyi/Blog/src/Models/Tag.php
  43. 70 0
      packages/Longyi/Blog/src/Providers/BlogServiceProvider.php
  44. 15 0
      packages/Longyi/Blog/src/Providers/ModuleServiceProvider.php
  45. 150 0
      packages/Longyi/Blog/src/Repositories/BlogCategoryRepository.php
  46. 84 0
      packages/Longyi/Blog/src/Repositories/BlogCommentRepository.php
  47. 211 0
      packages/Longyi/Blog/src/Repositories/BlogRepository.php
  48. 150 0
      packages/Longyi/Blog/src/Repositories/BlogTagRepository.php
  49. 221 0
      packages/Longyi/Blog/src/Resources/assets/css/custom-blog.css
  50. 12 0
      packages/Longyi/Blog/src/Resources/assets/images/Icon-Temp-Active.svg
  51. 12 0
      packages/Longyi/Blog/src/Resources/assets/images/Icon-Temp.svg
  52. 0 0
      packages/Longyi/Blog/src/Resources/assets/js/app.js
  53. 17 0
      packages/Longyi/Blog/src/Resources/assets/sass/admin.scss
  54. 0 0
      packages/Longyi/Blog/src/Resources/assets/sass/default.scss
  55. 0 0
      packages/Longyi/Blog/src/Resources/assets/sass/velocity.scss
  56. 130 0
      packages/Longyi/Blog/src/Resources/lang/en/app.php
  57. 125 0
      packages/Longyi/Blog/src/Resources/lang/pt_BR/app.php
  58. 125 0
      packages/Longyi/Blog/src/Resources/lang/tr/app.php
  59. 646 0
      packages/Longyi/Blog/src/Resources/views/admin/blogs/create.blade.php
  60. 670 0
      packages/Longyi/Blog/src/Resources/views/admin/blogs/edit.blade.php
  61. 28 0
      packages/Longyi/Blog/src/Resources/views/admin/blogs/index.blade.php
  62. 416 0
      packages/Longyi/Blog/src/Resources/views/admin/categories/create.blade.php
  63. 456 0
      packages/Longyi/Blog/src/Resources/views/admin/categories/edit.blade.php
  64. 28 0
      packages/Longyi/Blog/src/Resources/views/admin/categories/index.blade.php
  65. 240 0
      packages/Longyi/Blog/src/Resources/views/admin/comments/edit.blade.php
  66. 28 0
      packages/Longyi/Blog/src/Resources/views/admin/comments/index.blade.php
  67. 269 0
      packages/Longyi/Blog/src/Resources/views/admin/import/index.blade.php
  68. 417 0
      packages/Longyi/Blog/src/Resources/views/admin/setting/index.blade.php
  69. 369 0
      packages/Longyi/Blog/src/Resources/views/admin/tags/create.blade.php
  70. 411 0
      packages/Longyi/Blog/src/Resources/views/admin/tags/edit.blade.php
  71. 28 0
      packages/Longyi/Blog/src/Resources/views/admin/tags/index.blade.php
  72. 1200 0
      packages/Longyi/Blog/src/Resources/views/custom-css/custom-css.blade.php
  73. 153 0
      packages/Longyi/Blog/src/Resources/views/shop/author/index.blade.php
  74. 167 0
      packages/Longyi/Blog/src/Resources/views/shop/category/index.blade.php
  75. 63 0
      packages/Longyi/Blog/src/Resources/views/shop/comment/list.blade.php
  76. 13 0
      packages/Longyi/Blog/src/Resources/views/shop/default/index.blade.php
  77. 167 0
      packages/Longyi/Blog/src/Resources/views/shop/tag/index.blade.php
  78. 153 0
      packages/Longyi/Blog/src/Resources/views/shop/velocity/index.blade.php
  79. 297 0
      packages/Longyi/Blog/src/Resources/views/shop/velocity/view.blade.php
  80. 148 0
      packages/Longyi/Blog/src/Routes/admin-routes.php
  81. 71 0
      packages/Longyi/Blog/src/Routes/shop-routes.php
  82. 126 0
      packages/Longyi/Blog/src/Validations/BlogCategoryUniqueSlug.php
  83. 127 0
      packages/Longyi/Blog/src/Validations/BlogTagUniqueSlug.php
  84. 127 0
      packages/Longyi/Blog/src/Validations/BlogUniqueSlug.php
  85. 24 0
      packages/Longyi/Blog/webpack.mix.js
  86. 13 6
      packages/Webkul/BagistoApi/src/Models/CustomerOrderItem.php
  87. 113 0
      packages/Webkul/BagistoApi/tests/Feature/GraphQL/CustomerOrderTest.php
  88. 53 0
      packages/Webkul/BagistoApi/tests/Unit/Models/CustomerOrderItemBaseImageTest.php

+ 6 - 6
bootstrap/cache/services.php

@@ -53,9 +53,9 @@
     49 => 'ApiPlatform\\Laravel\\Eloquent\\ApiPlatformEventProvider',
     50 => 'App\\Providers\\AppServiceProvider',
     51 => 'Webkul\\Admin\\Providers\\AdminServiceProvider',
-    52 => 'Longyi\\Core\\Providers\\LongyiCoreServiceProvider',
-    53 => 'Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider',
-    54 => 'Longyi\\FrontMenu\\Providers\\FrontMenuServiceProvider',
+    52 => 'Longyi\\Blog\\Providers\\BlogServiceProvider',
+    53 => 'Longyi\\Core\\Providers\\LongyiCoreServiceProvider',
+    54 => 'Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider',
     55 => 'Longyi\\Email\\Providers\\EmailServiceProvider',
     56 => 'Longyi\\ImageUpload\\Providers\\ImageUploadServiceProvider',
     57 => 'Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider',
@@ -140,9 +140,9 @@
     32 => 'ApiPlatform\\Laravel\\Eloquent\\ApiPlatformEventProvider',
     33 => 'App\\Providers\\AppServiceProvider',
     34 => 'Webkul\\Admin\\Providers\\AdminServiceProvider',
-    35 => 'Longyi\\Core\\Providers\\LongyiCoreServiceProvider',
-    36 => 'Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider',
-    37 => 'Longyi\\FrontMenu\\Providers\\FrontMenuServiceProvider',
+    35 => 'Longyi\\Blog\\Providers\\BlogServiceProvider',
+    36 => 'Longyi\\Core\\Providers\\LongyiCoreServiceProvider',
+    37 => 'Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider',
     38 => 'Longyi\\Email\\Providers\\EmailServiceProvider',
     39 => 'Longyi\\ImageUpload\\Providers\\ImageUploadServiceProvider',
     40 => 'Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider',

+ 1 - 0
bootstrap/providers.php

@@ -10,6 +10,7 @@ return [
      * Webkul's service providers.
      */
     Webkul\Admin\Providers\AdminServiceProvider::class,
+    Longyi\Blog\Providers\BlogServiceProvider::class,
     Longyi\Core\Providers\LongyiCoreServiceProvider::class,
     Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
     Longyi\FrontMenu\Providers\FrontMenuServiceProvider::class,

+ 1 - 0
composer.json

@@ -77,6 +77,7 @@
             "App\\": "app/",
             "Database\\Factories\\": "database/factories/",
             "Database\\Seeders\\": "database/seeders/",
+            "Longyi\\Blog\\": "packages/Longyi/Blog/src/",
             "Longyi\\Core\\": "packages/Longyi/Core/src/",
             "Longyi\\DynamicMenu\\": "packages/Longyi/DynamicMenu/src/",
             "Longyi\\FrontMenu\\": "packages/Longyi/FrontMenu/src/",

+ 101 - 0
packages/Longyi/Blog/README.md

@@ -0,0 +1,101 @@
+# WebbyCrown Blog Extension for Bagisto
+
+## 1. Introduction:
+
+"Blog for Bagisto" extension offers free, advanced-level features allowing seamless integration of blogs into Bagisto stores. It includes SEO-friendly URLs, categorized blogs, responsive images, comment widgets, customizable widgets, and blogs import/export.
+
+* SEO-friendly URL by navigating blog pages by page number and organizing posts by primary category and unique slug.
+* Blogs organized by category, tag, and author, enhancing user navigation and enabling focused browsing within specific content categories.
+* Responsive images across various devices and screen sizes for optimal user experience.
+* Show categories and tags with the number of posts associated.
+* Allows choice to enable/disable comment widget independently for each post, providing control over user interaction on specific blog entries.
+* Various widgets available: recent posts on homepage, related posts on detail page, and recent posts with nested replies support.
+* Enables posts and comments to support nested replies, facilitating organized discussions with threaded responses for enhanced engagement and interaction.
+* Allows users to designate a specific date for publishing individual blog posts within the Bagisto extension.
+* import/export features for easy content management: upload CSV files to add or update posts and export data for backup or transfer, streamlining blog organization and upkeep.
+
+
+
+## 2. Requirements:
+
+* **PHP**: 8.0 or higher.
+* **Bagisto**: v2.0.*
+* **Composer**: 1.6.5 or higher.
+
+## 3. Installation:
+
+- Run the following command
+```
+composer require webbycrown/blog-for-bagisto
+```
+
+- Run these commands below to complete the setup
+```
+composer dump-autoload
+```
+
+- Run these commands below to complete the setup
+```
+php artisan migrate
+```
+```
+php artisan storage:link
+```
+```
+php artisan optimize:clear
+```
+```
+php artisan vendor:publish --all
+```
+
+- Now to use the admin side:
+```
+https://example.com/admin/blog
+```
+
+- Now to use the eCommerce side:
+```
+https://example.com/blog
+```
+## 📊 Changelog
+
+### v1.0.1
+
+- 🖼️ **Tag Image Support** - Added image upload functionality for blog tags
+- 📸 Image upload capability in admin tag creation and edit forms with preview
+- 🎨 Automatic image conversion to **WebP format** for optimized performance
+- 📁 Organized image storage in structured directories (`blog-tag/{tag_id}/`)
+- ✅ Image validation supporting multiple formats (bmp, jpeg, jpg, png, webp)
+- 🔄 Enhanced **BlogTagRepository** with comprehensive image management
+- 🎯 Updated Tag model with `image_url` accessor for storage URL generation
+- 💎 Improved admin UI layout with better styling for tag management
+- 🛍️ Shop frontend now displays custom tag images instead of placeholder banners
+- 🧹 Automatic cleanup of old images when updating or deleting tags
+- 🐛 Code quality improvements based on Bagisto community feedback
+- 🔧 Fixed missing accessor method in Tag model to prevent display errors
+
+---
+
+### v1.0.0
+
+- ✨ Initial release of **Blog for Bagisto** extension
+- 🔍 SEO-friendly URL structure with page navigation and category organization
+- 📂 Blog organization by category, tag, and author for enhanced navigation
+- 📱 Responsive image support across all devices and screen sizes
+- 🏷️ Category and tag display with associated post counts
+- 💬 Configurable comment widget for individual posts with nested reply support
+- 🎛️ Multiple widgets: recent posts, related posts, and nested replies
+- 📅 Scheduled publishing for individual blog posts
+- 📤 Import/Export functionality via CSV for easy content management
+- 🌍 Multi-language support (English, Portuguese BR, Turkish)
+- 🎨 Admin panel for complete blog management
+- 🛒 Shop frontend with default and Velocity theme support
+- 🔎 SEO meta tags support for all blog content
+- 🗑️ Soft delete functionality for blogs, categories, tags, and comments
+- 🚀 Published to Composer: [webbycrown/blog-for-bagisto](https://packagist.org/packages/webbycrown/blog-for-bagisto)
+
+---
+
+<div align="center">
+  <strong>Made with ❤️ by <a href="https://webbycrown.com">WebbyCrown</a></strong>
+</div>

+ 30 - 0
packages/Longyi/Blog/composer.json

@@ -0,0 +1,30 @@
+{
+    "name": "webbycrown/blog-for-bagisto",
+    "description": "Blog for Bagisto",
+    "version": "1.0.1",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "WebbyCrown Solutions",
+            "email": "info@webbycrown.com"
+        }
+    ],
+    "require": {
+        "php": "^8.1"
+    },
+    "autoload": {
+        "psr-4": {
+            "Longyi\\Blog\\": "src/"
+        }
+    },
+    "extra": {
+        "laravel": {
+            "providers": [
+                "Longyi\\Blog\\Providers\\BlogServiceProvider"
+            ],
+            "aliases": {}
+        }
+    },
+    "minimum-stability": "stable"
+}

+ 17 - 0
packages/Longyi/Blog/package.json

@@ -0,0 +1,17 @@
+{
+    "scripts": {
+        "dev": "npm run development",
+        "development": "cross-env NODE_ENV=development          node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
+        "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",
+        "watch-poll": "npm run watch -- --watch-poll",
+        "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",
+        "prod": "npm run production",
+        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
+    },
+
+    "devDependencies": {
+        "cross-env": "^5.1.4",
+        "laravel-mix": "^2.1",
+        "laravel-mix-merge-manifest": "^0.1.1"
+    }
+}

+ 96 - 0
packages/Longyi/Blog/src/Config/acl.php

@@ -0,0 +1,96 @@
+<?php
+
+return [
+    [
+        'key' => 'blog',
+        'name' => 'Blog',
+        'route' => 'admin.blog.index',
+        'sort' => 3
+    ], [
+        'key'   => 'blog.blogs',
+        'name'  => 'Blogs',
+        'route' => 'admin.blog.index',
+        'sort'  => 1,
+    ], [
+        'key'   => 'blog.blogs.create',
+        'name'  => 'Add',
+        'route' => 'admin.blog.create',
+        'sort'  => 1,
+    ], [
+        'key'   => 'blog.blogs.edit',
+        'name'  => 'Edit',
+        'route' => 'admin.blog.edit',
+        'sort'  => 2,
+    ], [
+        'key'   => 'blog.blogs.delete',
+        'name'  => 'Delete',
+        'route' => 'admin.blog.delete',
+        'sort'  => 3,
+    ], [
+        'key'   => 'blog.category',
+        'name'  => 'Category',
+        'route' => 'admin.blog.category.index',
+        'sort'  => 2,
+    ], [
+        'key'   => 'blog.category.create',
+        'name'  => 'Create',
+        'route' => 'admin.blog.category.create',
+        'sort'  => 1,
+    ], [
+        'key'   => 'blog.category.edit',
+        'name'  => 'Edit',
+        'route' => 'admin.blog.category.edit',
+        'sort'  => 2,
+    ], [
+        'key'   => 'blog.category.delete',
+        'name'  => 'Delete',
+        'route' => 'admin.blog.category.delete',
+        'sort'  => 3,
+    ], [
+        'key'   => 'blog.tag',
+        'name'  => 'Tag',
+        'route' => 'admin.blog.tag.index',
+        'sort'  => 3,
+    ], [
+        'key'   => 'blog.tag.create',
+        'name'  => 'Create',
+        'route' => 'admin.blog.tag.create',
+        'sort'  => 1,
+    ], [
+        'key'   => 'blog.tag.edit',
+        'name'  => 'Edit',
+        'route' => 'admin.blog.tag.edit',
+        'sort'  => 2,
+    ], [
+        'key'   => 'blog.tag.delete',
+        'name'  => 'Delete',
+        'route' => 'admin.blog.tag.delete',
+        'sort'  => 3,
+    ], [
+        'key'   => 'blog.comment',
+        'name'  => 'Comment',
+        'route' => 'admin.blog.comment.index',
+        'sort'  => 4,
+    ], [
+        'key'   => 'blog.comment.edit',
+        'name'  => 'Edit',
+        'route' => 'admin.blog.comment.edit',
+        'sort'  => 1,
+    ], [
+        'key'   => 'blog.comment.delete',
+        'name'  => 'Delete',
+        'route' => 'admin.blog.comment.delete',
+        'sort'  => 2,
+    ], [
+        'key'   => 'blog.setting',
+        'name'  => 'Setting',
+        'route' => 'admin.blog.setting.index',
+        'sort'  => 5,
+    ], [
+        'key'   => 'blog.import_export',
+        'name'  => 'Import/Export',
+        'route' => 'admin.blog.import.export',
+        'sort'  => 6,
+    ]
+
+];

+ 53 - 0
packages/Longyi/Blog/src/Config/menu.php

@@ -0,0 +1,53 @@
+<?php
+
+return [
+
+    /**
+     * Blog.
+     */
+    [
+        'key' => 'blog',
+        'name' => 'Blog',
+        'route' => 'admin.blog.index',
+        'sort' => 3,
+        // 'icon' => 'icon-blog',
+        'icon' => 'icon-attribute',
+    ], [
+        'key'        => 'blog.blogs',
+        'name'       => 'Blogs',
+        'route'      => 'admin.blog.index',
+        'sort'       => 1,
+        'icon'       => '',
+    ], [
+        'key'        => 'blog.category',
+        'name'       => 'Category',
+        'route'      => 'admin.blog.category.index',
+        'sort'       => 2,
+        'icon'       => '',
+    ], [
+        'key'        => 'blog.tag',
+        'name'       => 'Tag',
+        'route'      => 'admin.blog.tag.index',
+        'sort'       => 3,
+        'icon'       => '',
+    ], [
+        'key'        => 'blog.comment',
+        'name'       => 'Comment',
+        'route'      => 'admin.blog.comment.index',
+        'sort'       => 4,
+        'icon'       => '',
+    ], [
+        'key'        => 'blog.setting',
+        'name'       => 'Setting',
+        'route'      => 'admin.blog.setting.index',
+        'sort'       => 5,
+        'icon'       => '',
+    ], [
+        'key'        => 'blog.import_export',
+        'name'       => 'Import/Export',
+        'route'      => 'admin.blog.import.export',
+        'sort'       => 6,
+        'icon'       => '',
+    ],
+
+];

+ 7 - 0
packages/Longyi/Blog/src/Contracts/Blog.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Longyi\Blog\Contracts;
+
+interface Blog
+{
+}

+ 7 - 0
packages/Longyi/Blog/src/Contracts/Category.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Longyi\Blog\Contracts;
+
+interface Category
+{
+}

+ 7 - 0
packages/Longyi/Blog/src/Contracts/Comment.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Longyi\Blog\Contracts;
+
+interface Comment
+{
+}

+ 7 - 0
packages/Longyi/Blog/src/Contracts/Tag.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Longyi\Blog\Contracts;
+
+interface Tag
+{
+}

+ 50 - 0
packages/Longyi/Blog/src/Database/Migrations/2023_03_21_172616_create_blogs_table.php

@@ -0,0 +1,50 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('blogs', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->string('slug');
+            $table->string('short_description');
+            $table->longText('description');
+            $table->foreignId('channels');
+            $table->foreignId('default_category');
+            $table->string('categorys')->nullable();
+            $table->string('tags');
+            $table->string('author');
+            $table->foreignId('author_id')->default(0);
+            $table->string('src');
+            $table->string('locale');
+            $table->boolean('status');
+            $table->boolean('allow_comments');
+            $table->string('meta_title');
+            $table->string('meta_description');
+            $table->string('meta_keywords');
+            $table->dateTime('published_at');
+            $table->timestamps();
+            $table->softDeletes();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('blogs');
+    }
+};

+ 42 - 0
packages/Longyi/Blog/src/Database/Migrations/2023_03_21_175157_create_blog_categories_table.php

@@ -0,0 +1,42 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('blog_categories', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->string('slug');
+            $table->longText('description');
+            $table->string('image');
+            $table->boolean('status');
+            $table->foreignId('parent_id')->default(0)->nullable();
+            $table->string('locale');
+            $table->string('meta_title');
+            $table->string('meta_description');
+            $table->string('meta_keywords');
+            $table->timestamps();
+            $table->softDeletes();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('blog_categories');
+    }
+};

+ 41 - 0
packages/Longyi/Blog/src/Database/Migrations/2023_03_21_175231_create_blog_tags_table.php

@@ -0,0 +1,41 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('blog_tags', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->string('slug');
+            $table->string('description');
+            $table->string('image');
+            $table->boolean('status');
+            $table->string('locale');
+            $table->string('meta_title');
+            $table->string('meta_description');
+            $table->string('meta_keywords');
+            $table->timestamps();
+            $table->softDeletes();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('blog_tags');
+    }
+};

+ 39 - 0
packages/Longyi/Blog/src/Database/Migrations/2023_03_21_175251_create_blog_comments_table.php

@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('blog_comments', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('author');
+            $table->foreignId('post');
+            $table->string('name');
+            $table->string('email');
+            $table->string('comment');
+            $table->integer('parent_id')->defualt(0);
+            $table->boolean('status');
+            $table->timestamps();
+            $table->softDeletes();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('blog_comments');
+    }
+};

+ 32 - 0
packages/Longyi/Blog/src/Database/Migrations/2024_01_01_000000_add_m_src_to_blogs_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('blogs', function (Blueprint $table) {
+            $table->string('m_src')->nullable()->after('src');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('blogs', function (Blueprint $table) {
+            $table->dropColumn('m_src');
+        });
+    }
+};

+ 239 - 0
packages/Longyi/Blog/src/Datagrids/BlogDataGrid.php

@@ -0,0 +1,239 @@
+<?php
+
+namespace Longyi\Blog\Datagrids;
+
+use Illuminate\Support\Facades\DB;
+use Webkul\Core\Models\Channel;
+use Webkul\DataGrid\DataGrid;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+
+class BlogDataGrid extends DataGrid
+{
+    /**
+     * Set index columns, ex: id.
+     *
+     * @var int
+     */
+    protected $index = 'id';
+
+    /**
+     * Default sort order of datagrid.
+     *
+     * @var string
+     */
+    protected $sortOrder = 'desc';
+
+    /**
+     * Locale.
+     *
+     * @var string
+     */
+    protected $locale = 'all';
+
+    /**
+     * Channel.
+     *
+     * @var string
+     */
+    protected $channel = 'all';
+
+    /**
+     * Contains the keys for which extra filters to render.
+     *
+     * @var string[]
+     */
+    protected $extraFilters = [
+        'channels',
+        'locales',
+    ];
+
+    public function prepareQueryBuilder()
+    {
+        // $queryBuilder = DB::table('blogs')
+        //     ->select('blogs.id','blogs.name', 'blogs.slug', 'blogs.short_description', 'blogs.description', 'blogs.channels',
+        //         'blogs.default_category', 'blogs.categorys', 'blogs.published_at',
+        //         'category.name as category_name', 'blogs.author',
+        //         'blogs.tags', 'blogs.src', 'blogs.status', 'blogs.allow_comments', 'blogs.published_at',
+        //         'blogs.meta_title', 'blogs.meta_description', 'blogs.meta_keywords')
+        //     ->leftJoin('blog_categories as category', 'blogs.default_category', '=', 'category.id');
+
+        // return $queryBuilder;
+
+        $loggedIn_user = auth()->guard('admin')->user()->toarray();
+        $user_id = ( array_key_exists('id', $loggedIn_user) ) ? $loggedIn_user['id'] : 0;
+        $role = ( array_key_exists('role', $loggedIn_user) ) ? ( array_key_exists('name', $loggedIn_user['role']) ? $loggedIn_user['role']['name'] : 'Administrator' ) : 'Administrator';
+
+        $queryBuilder = DB::table('blogs');
+
+            if ( $role != 'Administrator' ) {
+                $queryBuilder->where('blogs.author_id', $user_id);
+            }
+            
+            $queryBuilder->select('blogs.id','blogs.name', 'blogs.slug', 'blogs.short_description', 'blogs.description', 'blogs.channels',
+                'blogs.default_category', 'blogs.categorys', 'blogs.published_at','blogs.author',
+                'blogs.tags', 'blogs.src', 'blogs.status', 'blogs.allow_comments', 'blogs.published_at',
+                'blogs.meta_title', 'blogs.meta_description', 'blogs.meta_keywords');
+
+        return $queryBuilder;
+
+    }
+
+    public function prepareColumns()
+    {
+        $this->addColumn([
+            'index' => 'id',
+            'label' => trans('blog::app.datagrid.id'),
+            'type' => 'integer',
+            'searchable' => false,
+            'sortable' => true,
+            'filterable' => true,
+        ]);
+
+        $this->addColumn([
+            'index' => 'name',
+            'label' => trans('blog::app.datagrid.name'),
+            'type' => 'string',
+            'searchable' => true,
+            'sortable' => true,
+            'filterable' => true,
+        ]);
+
+        $this->addColumn([
+            'index' => 'default_category',
+            'label' => trans('blog::app.datagrid.category'),
+            'type' => 'string',
+            'searchable' => true,
+            'sortable' => true,
+            'filterable' => true,
+            'closure' => function ($value) {
+                $categorys = '-';
+                $categories_ids = array_values( array_unique( array_merge( explode( ',', $value->default_category ), explode( ',', $value->categorys ) ) ) );
+                if ( is_array($categories_ids) && !empty($categories_ids) && count($categories_ids) > 0 ) {
+                    $categories = Category::whereIn('id', $categories_ids)->get();
+                    $categories_names = ( !empty($categories) && count($categories) > 0 ) ? $categories->pluck('name')->toarray() : array();
+                    $categorys = ( !empty($categories_names) && count($categories_names) ) ? implode(', ', $categories_names) : '-';
+                }
+                return $categorys;
+            },
+        ]);
+
+        $this->addColumn([
+            'index' => 'tags',
+            'label' => 'Tags',
+            'type' => 'string',
+            'searchable' => true,
+            'sortable' => true,
+            'filterable' => true,
+            'closure' => function ($value) {
+                $tags = '-';
+                $tags_ids = array_values( array_unique( explode( ',', $value->tags ) ) );
+                if ( is_array($tags_ids) && !empty($tags_ids) && count($tags_ids) > 0 ) {
+                    $tag_deatils = Tag::whereIn('id', $tags_ids)->get();
+                    $tags_names = ( !empty($tag_deatils) && count($tag_deatils) > 0 ) ? $tag_deatils->pluck('name')->toarray() : array();
+                    $tags = ( !empty($tags_names) && count($tags_names) ) ? implode(', ', $tags_names) : '-';
+                }
+                return $tags;
+            },
+        ]);
+
+        $this->addColumn([
+            'index' => 'status',
+            'label' => trans('blog::app.datagrid.status'),
+            'type' => 'boolean',
+            'searchable' => true,
+            'sortable' => true,
+            'filterable' => true,
+            'closure' => function ($value) {
+                if ($value->status == 1) {
+                    return '<span class="badge badge-md badge-success label-active">' . trans('blog::app.blog.status-true') . '</span>';
+                } else {
+                    return '<span class="badge badge-md badge-danger label-info">' . trans('blog::app.blog.status-false') . '</span>';
+                }
+            },
+        ]);
+
+        $this->addColumn([
+            'index' => 'allow_comments',
+            'label' => trans('blog::app.datagrid.allow_comments'),
+            'type' => 'boolean',
+            'searchable' => true,
+            'sortable' => true,
+            'filterable' => true,
+            'closure' => function ($value) {
+                if ($value->allow_comments == 1) {
+                    return '<span class="badge badge-md badge-success label-active">' . trans('blog::app.blog.yes') . '</span>';
+                } else {
+                    return '<span class="badge badge-md badge-danger label-info">' . trans('blog::app.blog.no') . '</span>';
+                }
+            },
+        ]);
+
+        $this->addColumn([
+            'index' => 'published_at',
+            'label' => 'Published At',
+            'type' => 'datetime',
+            'searchable' => true,
+            'sortable' => true,
+            'filterable' => true,
+            'closure' => function ($value) {
+                if ( $value->published_at != '' && $value->published_at != null ) {
+                    return date_format( date_create($value->published_at), 'j F, Y' );
+                } else {
+                    return '-';
+                }
+            },
+        ]);
+
+        $this->addColumn([
+            'index' => 'author',
+            'label' => 'Author',
+            'type' => 'string',
+            'searchable' => true,
+            'sortable' => true,
+            'filterable' => true,
+        ]);
+
+    }
+
+    public function prepareActions()
+    {
+        if (bouncer()->hasPermission('blog.blogs.edit')) {
+            $this->addAction([
+                'title' => 'edit',
+                'method' => 'GET',
+                'icon' => 'icon-edit',
+                'route' => 'admin.blog.edit',
+                'url'    => function ($row) {
+                    return route('admin.blog.edit', $row->id);
+                },
+            ]);
+        }
+
+        if (bouncer()->hasPermission('blog.blogs.delete')) {
+            $this->addAction([
+                'title' => 'delete',
+                'method' => 'POST',
+                'icon' => 'icon-delete',
+                'route' => 'admin.blog.delete',
+                'url'    => function ($row) {
+                    return route('admin.blog.delete', $row->id);
+                },
+            ]);
+        }
+    }
+
+    public function prepareMassActions()
+    {
+        if (bouncer()->hasPermission('blog.blogs.delete')) {
+            $this->addMassAction([
+                'type'   => 'delete',
+                'label'  => trans('admin::app.datagrid.delete'),
+                'title'  => 'Delete',
+                'action' => route('admin.blog.massdelete'),
+                'url' => route('admin.blog.massdelete'),
+                'method' => 'POST',
+            ]);
+        }
+    }
+}

+ 151 - 0
packages/Longyi/Blog/src/Datagrids/CategoryDataGrid.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace Longyi\Blog\Datagrids;
+
+use Illuminate\Support\Facades\DB;
+use Webkul\Core\Models\Channel;
+use Webkul\DataGrid\DataGrid;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+
+class CategoryDataGrid extends DataGrid
+{
+    /**
+     * Set index columns, ex: id.
+     *
+     * @var int
+     */
+    protected $index = 'id';
+
+    /**
+     * Default sort order of datagrid.
+     *
+     * @var string
+     */
+    protected $sortOrder = 'desc';
+
+    /**
+     * Locale.
+     *
+     * @var string
+     */
+    protected $locale = 'all';
+
+    /**
+     * Channel.
+     *
+     * @var string
+     */
+    protected $channel = 'all';
+
+    /**
+     * Contains the keys for which extra filters to render.
+     *
+     * @var string[]
+     */
+    protected $extraFilters = [
+        'channels',
+        'locales',
+    ];
+
+    public function prepareQueryBuilder()
+    {
+        $queryBuilder = DB::table('blog_categories')
+            ->select('id')
+            ->addSelect('id', 'name', 'slug', 'status', 'description', 'meta_title', 'meta_description', 'meta_keywords', 'parent_id');
+
+        return $queryBuilder;
+    }
+
+    public function prepareColumns()
+    {
+        $this->addColumn([
+            'index'      => 'id',
+            'label'      => trans('blog::app.datagrid.id'),
+            'type'       => 'integer',
+            'searchable' => false,
+            'sortable'   => true,
+            'filterable' => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'name',
+            'label'      => trans('blog::app.datagrid.name'),
+            'type'       => 'string',
+            'searchable' => true,
+            'sortable'   => true,
+            'filterable' => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'parent_id',
+            'label'      => 'Parent',
+            'type'       => 'string',
+            'searchable' => true,
+            'sortable'   => true,
+            'filterable' => true,
+            'closure'    => function ($value) {
+                $parent_data = Category::where('id', (int)$value->parent_id)->first();
+                $parent_category_name = ( $parent_data && isset($parent_data->name) && !empty($parent_data->name) && !is_null($parent_data->name) ) ? $parent_data->name : '-';
+                return $parent_category_name;
+            },
+        ]);
+
+        $this->addColumn([
+            'index'      => 'status',
+            'label'      => trans('blog::app.datagrid.status'),
+            'type'       => 'boolean',
+            'searchable' => true,
+            'sortable'   => true,
+            'filterable' => true,
+            'closure'    => function ($value) {
+                if ($value->status == 1) {
+                    return '<span class="badge badge-md badge-success label-active">' . trans('blog::app.category.status-true') . '</span>';
+                } else {
+                    return '<span class="badge badge-md badge-danger label-info">' . trans('blog::app.category.status-false') . '</span>';
+                }
+            },
+        ]);
+    }
+
+    public function prepareActions()
+    {
+        if (bouncer()->hasPermission('blog.category.edit')) {
+            $this->addAction([
+                'title' => 'edit',
+                'method' => 'GET',
+                'route' => 'admin.blog.category.edit',
+                'icon' => 'icon-edit',
+                'url'    => function ($row) {
+                    return route('admin.blog.category.edit', $row->id);
+                },
+            ]);
+        }
+
+        if (bouncer()->hasPermission('blog.category.delete')) {
+            $this->addAction([
+                'title' => 'delete',
+                'method' => 'POST',
+                'route' => 'admin.blog.category.delete',
+                'icon' => 'icon-delete',
+                'url'    => function ($row) {
+                    return route('admin.blog.category.delete', $row->id);
+                },
+            ]);
+        }
+    }
+
+    public function prepareMassActions()
+    {
+        if (bouncer()->hasPermission('blog.category.delete')) {
+            $this->addMassAction([
+                'type'   => 'delete',
+                'label'  => trans('admin::app.datagrid.delete'),
+                'title'  => 'Delete',
+                'action' => route('admin.blog.category.massdelete'),
+                'url' => route('admin.blog.category.massdelete'),
+                'method' => 'POST',
+            ]);
+        }
+    }
+}

+ 151 - 0
packages/Longyi/Blog/src/Datagrids/CommentDataGrid.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace Longyi\Blog\Datagrids;
+
+use Illuminate\Support\Facades\DB;
+use Webkul\Core\Models\Channel;
+use Webkul\DataGrid\DataGrid;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Blog;
+
+class CommentDataGrid extends DataGrid
+{
+    protected $index = 'id';
+
+    protected $sortOrder = 'desc';
+
+    public function prepareQueryBuilder()
+    {
+        // $queryBuilder = DB::table('blog_comments')
+        //     ->select('blog_comments.id', 'blog_comments.post', 'blog_comments.author', 'blog_comments.email', 'blog_comments.comment', 'blog_comments.status', 'blog_comments.created_at');
+
+        // return $queryBuilder;
+
+        $loggedIn_user = auth()->guard('admin')->user()->toarray();
+        $user_id = ( array_key_exists('id', $loggedIn_user) ) ? $loggedIn_user['id'] : 0;
+        $role = ( array_key_exists('role', $loggedIn_user) ) ? ( array_key_exists('name', $loggedIn_user['role']) ? $loggedIn_user['role']['name'] : 'Administrator' ) : 'Administrator';
+
+        $queryBuilder = DB::table('blog_comments');
+
+            if ( $role != 'Administrator' ) {
+                $blogs = Blog::where('author_id', $user_id)->get();
+                $post_ids = ( !empty($blogs) && count($blogs) > 0 ) ? $blogs->pluck('id')->toarray() : array();
+                $queryBuilder->whereIn('blog_comments.post', $post_ids);
+            }
+
+            $queryBuilder->select('blog_comments.id', 'blog_comments.post', 'blog_comments.author', 'blog_comments.email', 'blog_comments.comment', 'blog_comments.status', 'blog_comments.created_at');
+
+        return $queryBuilder;
+    }
+
+    public function prepareColumns()
+    {
+        $this->addColumn([
+            'index'      => 'id',
+            'label'      => trans('blog::app.datagrid.id'),
+            'type'       => 'integer',
+            'searchable' => false,
+            'sortable'   => true,
+            'filterable' => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'post',
+            'label'      => trans('blog::app.datagrid.name'),
+            'type'       => 'string',
+            'searchable' => true,
+            'sortable'   => true,
+            'filterable' => false,
+            'closure'    => function ($value) {
+                $post = Blog::where('id', $value->post)->first();
+                $post_name = ( $post && isset($post->name) && !empty($post->name) && !is_null($post->name) ) ? $post->name : '-';
+                return $post_name;
+            },
+        ]);
+
+        $this->addColumn([
+            'index'      => 'comment',
+            'label'      => trans('blog::app.datagrid.content'),
+            'type'       => 'string',
+            'searchable' => false,
+            'sortable'   => true,
+            'filterable' => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'status',
+            'label'      => trans('blog::app.datagrid.status'),
+            'type'       => 'boolean',
+            'searchable' => true,
+            'sortable'   => true,
+            'filterable' => true,
+            'closure'    => function ($value) {
+                if ($value->status == 1) {
+                    return '<span class="badge badge-md badge-warning label-pending">' . trans('blog::app.datagrid.pending') . '</span>';
+                } elseif ($value->status == 2) {
+                    return '<span class="badge badge-md badge-success label-active">' . trans('blog::app.datagrid.approved') . '</span>';
+                } elseif ($value->status == 0) {
+                    return '<span class="badge badge-md badge-danger label-canceled">' . trans('blog::app.datagrid.rejected') . '</span>';
+                }
+            },
+        ]);
+
+        $this->addColumn([
+            'index'      => 'created_at',
+            'label'      => trans('blog::app.datagrid.published_at'),
+            'type'       => 'datetime',
+            'searchable' => true,
+            'sortable'   => true,
+            'filterable' => true,
+            'closure' => function ($value) {
+                if ( $value->created_at != '' && $value->created_at != null ) {
+                    return date_format( date_create($value->created_at), 'j F, Y' );
+                } else {
+                    return '-';
+                }
+            },
+        ]);
+    }
+
+    public function prepareActions()
+    {
+        if (bouncer()->hasPermission('blog.comment.edit')) {
+            $this->addAction([
+                'title' => 'edit',
+                'method' => 'GET',
+                'route' => 'admin.blog.comment.edit',
+                'icon' => 'icon-edit',
+                'url'    => function ($row) {
+                    return route('admin.blog.comment.edit', $row->id);
+                },
+            ]);
+        }
+
+        if (bouncer()->hasPermission('blog.comment.delete')) {
+            $this->addAction([
+                'title' => 'delete',
+                'method' => 'POST',
+                'route' => 'admin.blog.comment.delete',
+                'icon' => 'icon-delete',
+                'url'    => function ($row) {
+                    return route('admin.blog.comment.delete', $row->id);
+                },
+            ]);
+        }
+    }
+
+    public function prepareMassActions()
+    {
+        if (bouncer()->hasPermission('blog.comment.delete')) {
+            $this->addMassAction([
+                'type'   => 'delete',
+                'label'  => trans('admin::app.datagrid.delete'),
+                'title'  => 'Delete',
+                'action' => route('admin.blog.comment.massdelete'),
+                'url' => route('admin.blog.comment.massdelete'),
+                'method' => 'POST',
+            ]);
+        }
+    }
+}

+ 134 - 0
packages/Longyi/Blog/src/Datagrids/TagDataGrid.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace Longyi\Blog\Datagrids;
+
+use Illuminate\Support\Facades\DB;
+use Webkul\Core\Models\Channel;
+use Webkul\DataGrid\DataGrid;
+
+class TagDataGrid extends DataGrid
+{
+    /**
+     * Set index columns, ex: id.
+     *
+     * @var int
+     */
+    protected $index = 'id';
+
+    /**
+     * Default sort order of datagrid.
+     *
+     * @var string
+     */
+    protected $sortOrder = 'desc';
+
+    /**
+     * Locale.
+     *
+     * @var string
+     */
+    protected $locale = 'all';
+
+    /**
+     * Channel.
+     *
+     * @var string
+     */
+    protected $channel = 'all';
+
+    /**
+     * Contains the keys for which extra filters to render.
+     *
+     * @var string[]
+     */
+    protected $extraFilters = [
+        'channels',
+        'locales',
+    ];
+
+    public function prepareQueryBuilder()
+    {
+        $queryBuilder = DB::table('blog_tags')->select('id')
+            ->addSelect('id', 'name', 'slug', 'description', 'status', 'meta_title', 'meta_description', 'meta_keywords');;
+
+        return $queryBuilder;
+    }
+
+    public function prepareColumns()
+    {
+        $this->addColumn([
+            'index'      => 'id',
+            'label'      => trans('blog::app.datagrid.id'),
+            'type'       => 'integer',
+            'searchable' => false,
+            'sortable'   => true,
+            'filterable' => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'name',
+            'label'      => trans('blog::app.datagrid.name'),
+            'type'       => 'string',
+            'searchable' => true,
+            'sortable'   => true,
+            'filterable' => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'status',
+            'label'      => trans('blog::app.datagrid.status'),
+            'type'       => 'boolean',
+            'searchable' => true,
+            'sortable'   => true,
+            'filterable' => true,
+            'closure'    => function ($value) {
+                if ($value->status == 1) {
+                    return '<span class="badge badge-md badge-success label-active">' . trans('blog::app.tag.status-true') . '</span>';
+                } else {
+                    return '<span class="badge badge-md badge-danger label-info">' . trans('blog::app.tag.status-false') . '</span>';
+                }
+            },
+        ]);
+    }
+
+    public function prepareActions()
+    {
+        if (bouncer()->hasPermission('blog.tag.edit')) {
+            $this->addAction([
+                'title' => 'edit',
+                'method' => 'GET',
+                'route' => 'admin.blog.tag.edit',
+                'icon' => 'icon-edit',
+                'url'    => function ($row) {
+                    return route('admin.blog.tag.edit', $row->id);
+                },
+            ]);
+        }
+
+        if (bouncer()->hasPermission('blog.tag.delete')) {
+            $this->addAction([
+                'title' => 'delete',
+                'method' => 'POST',
+                'route' => 'admin.blog.tag.delete',
+                'icon' => 'icon-delete',
+                'url'    => function ($row) {
+                    return route('admin.blog.tag.delete', $row->id);
+                },
+            ]);
+        }
+    }
+
+    public function prepareMassActions()
+    {
+        if (bouncer()->hasPermission('blog.tag.delete')) {
+            $this->addMassAction([
+                'type'   => 'delete',
+                'label'  => trans('admin::app.datagrid.delete'),
+                'title'  => 'Delete',
+                'action' => route('admin.blog.tag.massdelete'),
+                'url' => route('admin.blog.tag.massdelete'),
+                'method' => 'POST',
+            ]);
+        }
+    }
+}

+ 91 - 0
packages/Longyi/Blog/src/Export/BlogExport.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace Longyi\Blog\Export;
+
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Webkul\User\Models\Admin;
+use Maatwebsite\Excel\Concerns\FromCollection;
+use Maatwebsite\LaravelNovaExcel\Actions\ExportToExcel;
+use Maatwebsite\Excel\Concerns\WithHeadings;
+use DB;
+use URL;
+
+class BlogExport implements FromCollection, WithHeadings
+{
+
+    function __construct()
+    {
+        //
+    }
+    public function headings(): array
+    {
+        return [
+            'name',
+            'slug',
+            'short_description',
+            'description',
+            'category_slugs',
+            'tag_slugs',
+            'author_email',
+            'image_url',
+            'status',
+            'allow_comments',
+            'meta_title',
+            'meta_description',
+            'meta_keywords',
+            'published_at',
+        ];
+    }
+    public function collection()
+    {
+        return DB::table( 'blogs' )
+        ->select( 
+            'name',
+            'slug',
+            'short_description',
+            'description',
+            DB::raw( 'CONCAT(default_category, ",", IFNULL(categorys,"")) as category_slugs' ),
+            'tags as tag_slugs',
+            'author_id as author_email',
+            'src as image_url',
+            'status',
+            'allow_comments',
+            'meta_title',
+            'meta_description',
+            'meta_keywords',
+            'published_at' 
+        )->get()->each(function ($item) {
+
+            #categorys
+            $category_slugs = ( isset( $item->category_slugs ) && !empty( $item->category_slugs ) && !is_null( $item->category_slugs ) ) ? explode( ',', $item->category_slugs) : array();
+            $category_slugs_arr = array();
+            $if_cat_exist = Category::whereIn( 'id', $category_slugs )->get();
+            if ( $if_cat_exist && count( $if_cat_exist ) > 0 ) {
+                $category_slugs_arr = $if_cat_exist->pluck( 'slug' )->unique()->toarray();
+            }
+            $item->category_slugs = ( is_array( $category_slugs_arr ) ) ? implode( ',', $category_slugs_arr ) : '';
+
+            #tags
+            $tag_slugs = ( isset( $item->tag_slugs ) && !empty( $item->tag_slugs ) && !is_null( $item->tag_slugs ) ) ? explode( ',', $item->tag_slugs) : array();
+            $tag_slugs_arr = array();
+            $if_tag_exist = Tag::whereIn( 'id', $tag_slugs )->get();
+            if ( $if_tag_exist && count( $if_tag_exist ) > 0 ) {
+                $tag_slugs_arr = $if_tag_exist->pluck( 'slug' )->unique()->toarray();
+            }
+            $item->tag_slugs = ( is_array( $tag_slugs_arr ) ) ? implode( ',', $tag_slugs_arr ) : '';
+
+            #author
+            $author_id = ( isset( $item->author_email ) && !empty( $item->author_email ) && !is_null( $item->author_email ) ) ? (int)$item->author_email : 0;
+            $item->author_email = '';
+            $if_author_exist = Admin::where( 'id', $author_id )->first();
+            if ( $if_author_exist ) {
+                $item->author_email = $if_author_exist->email;
+            }
+
+            #image
+            $item->image_url = ( isset( $item->image_url ) && !empty( $item->image_url ) && !is_null( $item->image_url ) ) ? env( 'APP_URL' ) . '/storage/' . $item->image_url : '';
+        });
+    }
+}

+ 289 - 0
packages/Longyi/Blog/src/Http/Controllers/Admin/BlogController.php

@@ -0,0 +1,289 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Admin;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Longyi\Blog\Datagrids\BlogDataGrid;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Repositories\BlogRepository;
+use Webkul\User\Models\Admin;
+use Longyi\Blog\Http\Requests\BlogRequest;
+
+class BlogController extends Controller
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct(protected BlogRepository $blogRepository)
+    {
+        $this->middleware('admin');
+
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        if (request()->ajax()) {
+            return app(BlogDataGrid::class)->toJson();
+        }
+
+        return view($this->_config['view']);
+    }
+
+    public function gteBlogs()
+    {
+        try {
+            $loggedInUser = auth()->guard('admin')->user();
+
+            if (! $loggedInUser) {
+                return response()->json([
+                    'status_code' => 401,
+                    'status'      => 'error',
+                    'message'     => 'Unauthenticated.',
+                    'data'        => null,
+                ], 401);
+            }
+
+            $role = $loggedInUser->role->name ?? 'Administrator';
+
+            $query = Blog::query();
+
+            if ($role !== 'Administrator') {
+                $query->where('author_id', $loggedInUser->id);
+            }
+
+            $perPage = (int) request()->get('per_page', 20);
+
+            $blogs = $query->orderBy('id', 'DESC')->paginate($perPage);
+
+            return response()->json([
+                'status_code' => 200,
+                'status'      => 'success',
+                'message'     => 'get blogs successfully',
+                'data'        => [
+                    'items' => $blogs->items(),
+                    'meta'  => [
+                        'current_page' => $blogs->currentPage(),
+                        'last_page'    => $blogs->lastPage(),
+                        'per_page'     => $blogs->perPage(),
+                        'total'        => $blogs->total(),
+                    ],
+                ],
+            ], 200);
+        } catch (\Exception $e) {
+            return response()->json([
+                'status_code' => 500,
+                'status'      => 'error',
+                'message'     => $e->getMessage(),
+                'data'        => null,
+            ], 500);
+        }
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function create()
+    {
+        $locale = core()->getRequestedLocaleCode();
+
+        $categories = Category::all();
+
+        $additional_categories = Category::where('parent_id', 0)->where('status', 1)->get();
+
+        $tags = Tag::all();
+
+        $users = Admin::all();
+
+        return view($this->_config['view'], compact('categories', 'tags', 'users', 'additional_categories'))->with('locale', $locale);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function store(BlogRequest $blogRequest)
+    {
+
+        $data = request()->all();
+
+        if (array_key_exists('locale', $data) &&  is_array($data['locale'])) {
+            $data['locale'] = implode(',', $data['locale']);
+        }
+
+        if (array_key_exists('tags', $data) &&  is_array($data['tags'])) {
+            $data['tags'] = implode(',', $data['tags']);
+        }
+
+        if (array_key_exists('categorys', $data) && is_array($data['categorys'])) {
+            $data['categorys'] = implode(',', $data['categorys']);
+        }
+
+        $data['author'] = '';
+        if (is_array($data) && array_key_exists('author_id', $data) && isset($data['author_id']) && (int)$data['author_id'] > 0) {
+            $author_data = Admin::where('id', $data['author_id'])->first();
+            $data['author'] = ( $author_data && !empty($author_data) ) ? $author_data->name : '';
+        }
+
+        $result = $this->blogRepository->save($data);
+
+        if ($result) {
+            session()->flash('success', trans('admin::app.response.create-success', ['name' => 'Blog']));
+        } else {
+            session()->flash('success', trans('blog::app.blog.created-fault'));
+        }
+
+        return redirect()->route($this->_config['redirect']);
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\View\View
+     */
+    public function edit($id)
+    {
+        $loggedIn_user = auth()->guard('admin')->user()->toarray();
+        $user_id = ( array_key_exists('id', $loggedIn_user) ) ? $loggedIn_user['id'] : 0;
+        $role = ( array_key_exists('role', $loggedIn_user) ) ? ( array_key_exists('name', $loggedIn_user['role']) ? $loggedIn_user['role']['name'] : 'Administrator' ) : 'Administrator';
+
+        $blog = $this->blogRepository->findOrFail($id);
+
+        if ( $blog && $user_id != $blog->author_id && $role != 'Administrator' ) {
+            return redirect()->route('admin.blog.index');
+        }
+
+        $categories = Category::all();
+
+        $additional_categories = Category::where('parent_id', 0)->where('status', 1)->get();
+
+        $tags = Tag::all();
+
+        $users = Admin::all();
+
+        return view($this->_config['view'], compact('blog', 'categories', 'tags', 'users', 'additional_categories'));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(BlogRequest $blogRequest, $id)
+    {
+
+        $data = request()->all();
+
+        if (array_key_exists('locale', $data) && is_array($data['locale'])) {
+            $data['locale'] = implode(',', $data['locale']);
+        }
+        
+        if (array_key_exists('tags', $data) && is_array($data['tags'])) {
+            $data['tags'] = implode(',', $data['tags']);
+        }
+        
+        if (array_key_exists('categorys', $data) && is_array($data['categorys'])) {
+            $data['categorys'] = implode(',', $data['categorys']);
+        }
+
+        $data['author'] = '';
+        if (is_array($data) && array_key_exists('author_id', $data) && isset($data['author_id']) && (int)$data['author_id'] > 0) {
+            $author_data = Admin::where('id', $data['author_id'])->first();
+            $data['author'] = ( $author_data && !empty($author_data) ) ? $author_data->name : '';
+        }
+
+        $result = $this->blogRepository->updateItem($data, $id);
+
+        if ($result) {
+            session()->flash('success', trans('admin::app.response.update-success', ['name' => 'Blog']));
+        } else {
+            session()->flash('error', trans('blog::app.blog.updated-fault'));
+        }
+
+        return redirect()->route($this->_config['redirect']);
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        $this->blogRepository->findOrFail($id);
+
+        try {
+            $this->blogRepository->delete($id);
+
+            return response()->json(['message' => trans('admin::app.response.delete-success', ['name' => 'Blog'])]);
+        } catch (\Exception $e) {
+            report($e);
+        }
+
+        return response()->json(['message' => trans('admin::app.response.delete-failed', ['name' => 'Blog'])], 500);
+    }
+
+    /**
+     * Remove the specified resources from database.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function massDestroy()
+    {
+        $suppressFlash = false;
+
+        if (request()->isMethod('post')) {
+            // $indexes = explode(',', request()->input('indexes'));
+            $indexes = (array)request()->input('indices');
+
+            foreach ($indexes as $key => $value) {
+                try {
+                    $this->blogRepository->delete($value);
+                } catch (\Exception $e) {
+                    $suppressFlash = true;
+
+                    continue;
+                }
+            }
+
+            if (! $suppressFlash) {
+                session()->flash('success', trans('admin::app.datagrid.mass-ops.delete-success', ['resource' => 'Blog']));
+            } else {
+                session()->flash('info', trans('admin::app.datagrid.mass-ops.partial-action', ['resource' => 'Blog']));
+            }
+
+            return redirect()->back();
+        } else {
+            session()->flash('error', trans('admin::app.datagrid.mass-ops.method-error'));
+
+            return redirect()->back();
+        }
+    }
+}

+ 214 - 0
packages/Longyi/Blog/src/Http/Controllers/Admin/CategoryController.php

@@ -0,0 +1,214 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Admin;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Illuminate\Support\Facades\Session;
+use Longyi\Blog\Datagrids\CategoryDataGrid;
+use Longyi\Blog\Repositories\BlogCategoryRepository;
+use Longyi\Blog\Http\Requests\BlogCategoryRequest;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Comment;
+
+class CategoryController extends Controller
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct(protected BlogCategoryRepository $blogCategoryRepository)
+    {
+        $this->middleware('admin');
+
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        if (request()->ajax()) {
+            return app(CategoryDataGrid::class)->toJson();
+        }
+
+        return view($this->_config['view']);
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function create()
+    {
+        $locale = core()->getRequestedLocaleCode();
+
+        $categories = Category::where('parent_id', 0)->where('status', 1)->get();
+
+        return view($this->_config['view'], compact('categories'))->with('locale', $locale);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function store(BlogCategoryRequest $blogCategoryRequest)
+    {
+        /*$this->validate(request(), [
+            'slug'                  => 'slug', 'unique',
+            'name'                  => 'required',
+            'image.*'               => 'required|mimes:bmp,jpeg,jpg,png,webp',
+            'description'           => 'required',
+        ]);*/
+
+        $data = request()->all();
+
+        if (is_array($data['locale'])) {
+            $data['locale'] = implode(',', $data['locale']);
+        }
+
+        $result = $this->blogCategoryRepository->save($data);
+
+        if ($result) {
+            session()->flash('success', trans('admin::app.catalog.categories.create-success', ['name' => 'Category']));
+        } else {
+            session()->flash('success', trans('blog::app.category.created-fault'));
+        }
+
+        return redirect()->route($this->_config['redirect']);
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\View\View
+     */
+    public function edit($id)
+    {
+        $categories = $this->blogCategoryRepository->findOrFail($id);
+
+        Session::put('bCatEditId', $id);
+
+        $categories_data = Category::where('parent_id', 0)->where('status', 1)->where('id', '!=', $id)->get();
+        
+        Session::remove('bCatEditId');
+
+        return view($this->_config['view'], compact('categories', 'categories_data'));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(BlogCategoryRequest $blogCategoryRequest, $id)
+    {
+        /*$this->validate(request(), [
+            'slug'                  => 'slug', 'unique',
+            'name'                  => 'required',
+            'image'                 => 'required',
+            'description'           => 'required',
+        ]);*/
+
+        $data = request()->all();
+
+        if (is_array($data['locale'])) {
+            $data['locale'] = implode(',', $data['locale']);
+        }
+
+        if (is_array($data) && array_key_exists('image', $data) && is_null(request()->image)) {
+            session()->flash('error', trans('blog::app.category.updated-fault'));
+
+            return redirect()->back();
+        }
+
+        $result = $this->blogCategoryRepository->updateItem($data, $id);
+
+        if ($result) {
+            session()->flash('success', trans('admin::app.catalog.categories.update-success', ['name' => 'Category']));
+        } else {
+            session()->flash('error', trans('blog::app.category.updated-fault'));
+        }
+
+        return redirect()->route($this->_config['redirect']);
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        $this->blogCategoryRepository->findOrFail($id);
+
+        try {
+            $this->blogCategoryRepository->delete($id);
+
+            return response()->json(['message' => trans('admin::app.catalog.categories.delete-success', ['name' => 'Category'])]);
+        } catch (\Exception $e) {
+            report($e);
+        }
+
+        return response()->json(['message' => trans('admin::app.catalog.categories.delete-failed', ['name' => 'Category'])], 500);
+    }
+
+    /**
+     * Remove the specified resources from database.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function massDestroy()
+    {
+        $suppressFlash = false;
+
+        if (request()->isMethod('post')) {
+            // $indexes = explode(',', request()->input('indices'));
+            $indexes = (array)request()->input('indices');
+
+            foreach ($indexes as $key => $value) {
+                try {
+                    $this->blogCategoryRepository->delete($value);
+                } catch (\Exception $e) {
+                    $suppressFlash = true;
+
+                    continue;
+                }
+            }
+
+            if (! $suppressFlash) {
+                session()->flash('success', trans('admin::app.catalog.categories.delete-success', ['resource' => 'Category']));
+            } else {
+                session()->flash('info', trans('admin::app.catalog.categories.delete-failed', ['resource' => 'Category']));
+            }
+
+            return redirect()->back();
+        } else {
+            session()->flash('error', trans('admin::app.catalog.categories.delete-failed'));
+
+            return redirect()->back();
+        }
+    }
+}

+ 190 - 0
packages/Longyi/Blog/src/Http/Controllers/Admin/CommentController.php

@@ -0,0 +1,190 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Admin;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Longyi\Blog\Datagrids\CommentDataGrid;
+use Longyi\Blog\Repositories\BlogCommentRepository;
+use Webkul\User\Models\Admin;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Comment;
+
+class CommentController extends Controller
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct(protected BlogCommentRepository $blogCommentRepository)
+    {
+        $this->middleware('admin');
+
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        if (request()->ajax()) {
+            return app(CommentDataGrid::class)->toJson();
+        }
+
+        return view($this->_config['view']);
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function create()
+    {
+        return view($this->_config['view']);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function store()
+    {
+        //
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\View\View
+     */
+    public function edit($id)
+    {
+        $comment = $this->blogCommentRepository->findOrFail($id);
+
+        $loggedIn_user = auth()->guard('admin')->user()->toarray();
+        $user_id = ( array_key_exists('id', $loggedIn_user) ) ? $loggedIn_user['id'] : 0;
+        $role = ( array_key_exists('role', $loggedIn_user) ) ? ( array_key_exists('name', $loggedIn_user['role']) ? $loggedIn_user['role']['name'] : 'Administrator' ) : 'Administrator';
+        if ( $role != 'Administrator' ) {
+            $blogs = Blog::where('author_id', $user_id)->get();
+            $post_ids = ( !empty($blogs) && count($blogs) > 0 ) ? $blogs->pluck('id')->toarray() : array();
+            $check_comment = Comment::where('id', $id)->whereIn('post', $post_ids)->first();
+            if (!$check_comment) {
+                return redirect()->route('admin.blog.comment.index');
+            }
+        }
+
+        $author_name = '';
+        $author_id = $comment && isset($comment->author) ? $comment->author : 0;
+        if ( (int)$author_id > 0 ) {
+            $author_data = Admin::find($author_id);
+            $author_name = $author_data && isset($author_data->name) ? $author_data->name : '';
+        }
+
+        $status_details = array(
+            array( 'id' => 1, 'name' => 'blog::app.comment.status-pending' ),
+            array( 'id' => 2, 'name' => 'blog::app.comment.status-approved' ),
+            array( 'id' => 0, 'name' => 'blog::app.comment.status-rejected' ),
+        );
+
+        return view($this->_config['view'], compact('comment', 'author_name', 'status_details'));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update($id)
+    {
+        $data = request()->all();
+
+        $result = $this->blogCommentRepository->updateItem($data, $id);
+
+        if ($result) {
+            session()->flash('success', trans('admin::app.response.update-success', ['name' => 'Comment']));
+        } else {
+            session()->flash('error', trans('blog::app.comment.updated-fault'));
+        }
+
+        return redirect()->route($this->_config['redirect']);
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        $this->blogCommentRepository->findOrFail($id);
+
+        try {
+            $this->blogCommentRepository->delete($id);
+
+            return response()->json(['message' => trans('admin::app.response.delete-success', ['name' => 'Comment'])]);
+        } catch (\Exception $e) {
+            report($e);
+        }
+
+        return response()->json(['message' => trans('admin::app.response.delete-failed', ['name' => 'Comment'])], 500);
+    }
+
+    /**
+     * Remove the specified resources from database.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function massDestroy()
+    {
+        $suppressFlash = false;
+
+        if (request()->isMethod('post')) {
+            // $indexes = explode(',', request()->input('indexes'));
+            $indexes = (array)request()->input('indices');
+
+            foreach ($indexes as $key => $value) {
+                try {
+                    $this->blogCommentRepository->delete($value);
+                } catch (\Exception $e) {
+                    $suppressFlash = true;
+
+                    continue;
+                }
+            }
+
+            if (! $suppressFlash) {
+                session()->flash('success', trans('admin::app.datagrid.mass-ops.delete-success', ['resource' => 'Comment']));
+            } else {
+                session()->flash('info', trans('admin::app.datagrid.mass-ops.partial-action', ['resource' => 'Comment']));
+            }
+
+            return redirect()->back();
+        } else {
+            session()->flash('error', trans('admin::app.datagrid.mass-ops.method-error'));
+
+            return redirect()->back();
+        }
+    }
+}

+ 88 - 0
packages/Longyi/Blog/src/Http/Controllers/Admin/ImportController.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Admin;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Webkul\User\Models\Admin;
+use Webkul\Core\Models\CoreConfig;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Comment;
+use Maatwebsite\Excel\Facades\Excel;
+use Longyi\Blog\Import\BlogImport;
+use Longyi\Blog\Export\BlogExport;
+use Session;
+
+class ImportController extends Controller
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->middleware('admin');
+
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        $errors_data = array();
+        if ( Session::has( 'import_errors' ) ){
+            if ( count( Session::get( 'import_errors' ) ) > 0 ) {
+                $errors_data = Session::get( 'import_errors' );
+                Session::put( 'import_errors', array() );
+            }
+        }
+
+        return view($this->_config['view'], compact( 'errors_data' ));
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function import()
+    {
+        try{
+            $files = request()->file('imoprt_file');
+            $import_data_arr = Excel::toArray(new BlogImport(), $files);
+            $total = count( $import_data_arr[0] );
+            if ( (int)$total > 0 ) {
+                $import_data = Excel::import(new BlogImport(), $files);
+                return redirect()->back()->with('success', 'Import blog successfully.');
+            }
+            return redirect()->back()->with('error', 'Uploaded File is Empty.');
+        } catch(\Exception $e) {
+            return redirect()->back()->with('error', 'Something went wrong. ['.$e->getMessage().']');
+        }
+
+    }
+
+    public function export()
+    {
+        return Excel::download(new BlogExport(), 'blog.csv');
+    }
+
+}

+ 154 - 0
packages/Longyi/Blog/src/Http/Controllers/Admin/SettingController.php

@@ -0,0 +1,154 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Admin;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Webkul\User\Models\Admin;
+use Webkul\Core\Models\CoreConfig;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Comment;
+
+class SettingController extends Controller
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->middleware('admin');
+
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        $post_orders = array(
+            'recent' => 'Recent',
+            'popularity' => 'Popularity',
+            'random' => 'Random',
+        );
+
+        $config_data_keys = array( 'blog_post_per_page', 'blog_post_maximum_related', 'blog_post_recent_order_by', 'blog_post_show_categories_with_count', 'blog_post_show_tags_with_count', 'blog_post_show_author_page', 'blog_post_enable_comment', 'blog_post_allow_guest_comment', 'blog_post_enable_comment_moderation', 'blog_post_maximum_nested_comment', 'blog_seo_meta_title', 'blog_seo_meta_keywords', 'blog_seo_meta_description' );
+
+        $setting_deatils = $settings = array();
+        $setting_datas = CoreConfig::whereIn('code', $config_data_keys)->get();
+        if ( !empty($setting_datas) && count($setting_datas) > 0 ) {
+            $setting_datas = $setting_datas->toarray();
+            foreach ($setting_datas as $setting_data) {
+                $setting_deatils[ $setting_data['code'] ] = $setting_data['value'];
+            }
+        }
+
+        foreach ($config_data_keys as $config_data_key) {
+            $settings[ $config_data_key ] = ( array_key_exists($config_data_key, $setting_deatils) ) ? $setting_deatils[ $config_data_key ] : '';
+        }
+
+        return view($this->_config['view'], compact('post_orders', 'settings'));
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function create()
+    {
+        // 
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function store()
+    {
+        $data = request()->all();
+    
+        $config_except_keys = array( 'switch_blog_post_show_categories_with_count', 'switch_blog_post_show_tags_with_count', 'switch_blog_post_show_author_page', 'switch_blog_post_enable_comment', 'switch_blog_post_allow_guest_comment', 'switch_blog_post_enable_comment_moderation' );
+
+        if ( !empty($data) && count($data) > 0 ) {
+            foreach ( $data as $data_key => $data_value ) {
+                if ( !in_array($data_key, $config_except_keys) ) {
+                    $CoreConfig = CoreConfig::where('code', $data_key)->first();
+                    if ( $CoreConfig ) {
+                        $CoreConfig->value = $data_value;
+                    } else {
+                        $CoreConfig = new CoreConfig();
+                        $CoreConfig->code = $data_key;
+                        $CoreConfig->value = $data_value;
+                    }
+                    $CoreConfig->save();
+                }
+            }
+        }
+        session()->flash('success', 'Save Blog Setting Successfully');
+        return redirect()->route($this->_config['redirect']);
+
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\View\View
+     */
+    public function edit($id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update($id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resources from database.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function massDestroy()
+    {
+        //
+    }
+
+}

+ 193 - 0
packages/Longyi/Blog/src/Http/Controllers/Admin/TagController.php

@@ -0,0 +1,193 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Admin;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Longyi\Blog\Datagrids\TagDataGrid;
+use Longyi\Blog\Repositories\BlogTagRepository;
+use Longyi\Blog\Http\Requests\BlogTagRequest;
+
+class TagController extends Controller
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct(protected BlogTagRepository $blogTagRepository)
+    {
+        $this->middleware('admin');
+
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        if (request()->ajax()) {
+            return app(TagDataGrid::class)->toJson();
+        }
+
+        return view($this->_config['view']);
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function create()
+    {
+        $locale = core()->getRequestedLocaleCode();
+
+        return view($this->_config['view'])->with('locale', $locale);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function store(BlogTagRequest $blogTagRequest)
+    {
+        /*$this->validate(request(), [
+            'slug'                  => 'slug', 'unique',
+            'name'                  => 'required',
+            'description'           => 'required',
+        ]);*/
+
+        $data = request()->all();
+
+        if (is_array($data['locale'])) {
+            $data['locale'] = implode(',', $data['locale']);
+        }
+
+        $result = $this->blogTagRepository->save($data);
+
+        if ($result) {
+            session()->flash('success', trans('admin::app.response.create-success', ['name' => 'Tag']));
+        } else {
+            session()->flash('success', trans('blog::app.tag.created-fault'));
+        }
+
+        return redirect()->route($this->_config['redirect']);
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\View\View
+     */
+    public function edit($id)
+    {
+        $tag = $this->blogTagRepository->findOrFail($id);
+
+        return view($this->_config['view'], compact('tag'));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(BlogTagRequest $blogTagRequest, $id)
+    {
+        /*$this->validate(request(), [
+            'slug'                  => 'slug', 'unique',
+            'name'                  => 'required',
+            'description'           => 'required',
+        ]);*/
+
+        $data = request()->all();
+
+        if (is_array($data['locale'])) {
+            $data['locale'] = implode(',', $data['locale']);
+        }
+
+        $result = $this->blogTagRepository->updateItem($data, $id);
+
+        if ($result) {
+            session()->flash('success', trans('admin::app.response.update-success', ['name' => 'Tag']));
+        } else {
+            session()->flash('error', trans('blog::app.tag.updated-fault'));
+        }
+
+        return redirect()->route($this->_config['redirect']);
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        $this->blogTagRepository->findOrFail($id);
+
+        try {
+            $this->blogTagRepository->delete($id);
+
+            return response()->json(['message' => trans('admin::app.response.delete-success', ['name' => 'Tag'])]);
+        } catch (\Exception $e) {
+            report($e);
+        }
+
+        return response()->json(['message' => trans('admin::app.response.delete-failed', ['name' => 'Tag'])], 500);
+    }
+
+    /**
+     * Remove the specified resources from database.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function massDestroy()
+    {
+        $suppressFlash = false;
+
+        if (request()->isMethod('post')) {
+            // $indexes = explode(',', request()->input('indexes'));
+            $indexes = (array)request()->input('indices');
+
+            foreach ($indexes as $key => $value) {
+                try {
+                    $this->blogTagRepository->delete($value);
+                } catch (\Exception $e) {
+                    $suppressFlash = true;
+
+                    continue;
+                }
+            }
+
+            if (! $suppressFlash) {
+                session()->flash('success', trans('admin::app.datagrid.mass-ops.delete-success', ['resource' => 'Tag']));
+            } else {
+                session()->flash('info', trans('admin::app.datagrid.mass-ops.partial-action', ['resource' => 'Tag']));
+            }
+
+            return redirect()->back();
+        } else {
+            session()->flash('error', trans('admin::app.datagrid.mass-ops.method-error'));
+
+            return redirect()->back();
+        }
+    }
+}

+ 420 - 0
packages/Longyi/Blog/src/Http/Controllers/Shop/API/Blogs/BlogController.php

@@ -0,0 +1,420 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Shop\API\Blogs;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Http\JsonResponse;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Comment;
+use Carbon\Carbon;
+
+class BlogController extends Controller
+{
+    /**
+     * Current locale code.
+     */
+    protected function locale(): string
+    {
+        // 优先使用当前请求的 locale,其次回退到应用默认 locale
+        return core()->getCurrentLocale()->code ?? config('app.locale');
+    }
+
+    /**
+     * 解析逗号分隔的 id 参数为去重后的正整数数组。
+     *
+     * 支持:单个 id "3"、多个 id "3,4,5"、空格、非法值过滤。
+     */
+    protected function parseIds($value): array
+    {
+        if ($value === null || $value === '') {
+            return [];
+        }
+
+        $ids = is_array($value) ? $value : explode(',', (string) $value);
+
+        return array_values(array_unique(array_filter(array_map(function ($id) {
+            $id = (int) trim($id);
+            return $id > 0 ? $id : null;
+        }, $ids))));
+    }
+
+    /**
+     * 统一成功响应。
+     */
+    protected function success($data = null, string $message = 'success', int $status = 200): JsonResponse
+    {
+        return response()->json([
+            'status_code' => $status,
+            'status'      => 'success',
+            'message'     => $message,
+            'data'        => $data,
+        ], $status);
+    }
+
+    /**
+     * 统一失败响应。
+     */
+    protected function error(string $message = 'error', int $status = 400, $data = null): JsonResponse
+    {
+        return response()->json([
+            'status_code' => $status,
+            'status'      => 'error',
+            'message'     => $message,
+            'data'        => $data,
+        ], $status);
+    }
+
+    /**
+     * 获取已发布的博客基础查询。
+     */
+    protected function publishedBlogs()
+    {
+        return Blog::where('published_at', '<=', Carbon::now()->format('Y-m-d H:i:s'))
+            ->where('status', 1)
+            ->where('locale', $this->locale());
+    }
+
+    /**
+     * 博客列表(支持 category/tag/search/per_page)。
+     *
+     * GET /api/v1/blog
+     * 参数:
+     *   - category: 分类 id,支持多个(逗号分隔),如 ?category=3,4
+     *   - tag: 标签 id,支持多个(逗号分隔),如 ?tag=1,2,3
+     *   - s: 搜索关键词
+     *   - per_page: 每页数量,默认 12
+     */
+    public function list()
+    {
+        try {
+            $req          = request()->all();
+            $categoryIds  = $this->parseIds($req['category'] ?? '');
+            $tagIds       = $this->parseIds($req['tag'] ?? '');
+            $search       = $req['s'] ?? '';
+
+            $blogs = $this->publishedBlogs();
+
+            if (! empty($categoryIds)) {
+                $blogs = $blogs->where(function ($q) use ($categoryIds) {
+                    foreach ($categoryIds as $categoryId) {
+                        $q->where('default_category', $categoryId)
+                            ->orWhereRaw('FIND_IN_SET(?, categorys)', [$categoryId]);
+                    }
+                });
+            }
+
+            if (! empty($tagIds)) {
+                $blogs = $blogs->where(function ($q) use ($tagIds) {
+                    foreach ($tagIds as $tagId) {
+                        $q->orWhereRaw('FIND_IN_SET(?, tags)', [$tagId]);
+                    }
+                });
+            }
+
+            if (! empty($search)) {
+                $blogs = $blogs->where(function ($q) use ($search) {
+                    $q->where('name', 'like', '%'.$search.'%')
+                        ->orWhere('short_description', 'like', '%'.$search.'%');
+                });
+            }
+
+            $perPage = (int) request()->get('per_page', 12);
+            $perPage = $perPage > 0 ? $perPage : 12;
+
+            $blogs = $blogs->orderBy('id', 'DESC')->paginate($perPage);
+
+            return $this->success([
+                'items' => $blogs->items(),
+                'meta'  => [
+                    'current_page' => $blogs->currentPage(),
+                    'last_page'    => $blogs->lastPage(),
+                    'per_page'     => $blogs->perPage(),
+                    'total'        => $blogs->total(),
+                ],
+            ], 'get blog successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+
+    /**
+     * 博客详情。
+     *
+     * GET /api/v1/blog/{id}
+     */
+    public function show($id)
+    {
+        try {
+            $blog = $this->publishedBlogs()->where('id', $id)->first();
+
+            if (! $blog) {
+                return $this->error('blog not found', 404);
+            }
+
+            // 附带分类、标签、相关博客、评论数
+            $blog->load('category');
+
+            $tagIds = $blog->tags ? array_filter(explode(',', $blog->tags)) : [];
+            $tags   = Tag::whereIn('id', $tagIds)->get();
+
+            $categoryIds = array_values(array_unique(array_filter(array_merge(
+                explode(',', (string) $blog->default_category),
+                explode(',', (string) $blog->categorys)
+            ))));
+            $categories = Category::whereIn('id', $categoryIds)->get();
+
+            $related = $this->related($blog->id, true);
+
+            $commentsCount = Comment::where('post', $blog->id)->where('status', 2)->count();
+
+            return $this->success([
+                'blog'           => $blog,
+                'tags'           => $tags,
+                'categories'     => $categories,
+                'related_blogs'  => $related,
+                'comments_count' => $commentsCount,
+            ], 'get blog successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+
+    /**
+     * 相关博客。
+     *
+     * GET /api/v1/blog/{id}/related
+     */
+    public function related($id, $returnData = false)
+    {
+        try {
+            $blog = Blog::where('id', $id)->first();
+
+            if (! $blog) {
+                if ($returnData) {
+                    return [];
+                }
+
+                return $this->error('blog not found', 404);
+            }
+
+            $categoryIds = array_values(array_unique(array_filter(array_merge(
+                explode(',', (string) $blog->default_category),
+                explode(',', (string) $blog->categorys)
+            ))));
+
+            $related = $this->publishedBlogs()->where('id', '!=', $blog->id);
+
+            if (! empty($categoryIds)) {
+                $related = $related->where(function ($q) use ($categoryIds) {
+                    foreach ($categoryIds as $key => $categoryId) {
+                        if ($key === 0) {
+                            $q->where('default_category', $categoryId)
+                                ->orWhereRaw('FIND_IN_SET(?, categorys)', [$categoryId]);
+                        } else {
+                            $q->orWhere('default_category', $categoryId)
+                                ->orWhereRaw('FIND_IN_SET(?, categorys)', [$categoryId]);
+                        }
+                    }
+                });
+            }
+
+            $perPage = (int) request()->get('per_page', 4);
+
+            $related = $related->orderBy('id', 'DESC')->paginate($perPage);
+
+            if ($returnData) {
+                return $related->items();
+            }
+
+            return $this->success([
+                'items' => $related->items(),
+                'meta'  => [
+                    'current_page' => $related->currentPage(),
+                    'last_page'    => $related->lastPage(),
+                    'per_page'     => $related->perPage(),
+                    'total'        => $related->total(),
+                ],
+            ], 'get related blogs successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+
+    /**
+     * 分类列表。
+     *
+     * GET /api/v1/blog/category
+     */
+    public function category_list()
+    {
+        try {
+            $categories = Category::where('status', 1)
+                ->where('locale', $this->locale())
+                ->orderBy('id', 'DESC')
+                ->get();
+
+            return $this->success($categories, 'get blog category successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+
+    /**
+     * 分类详情及其下博客列表。
+     *
+     * GET /api/v1/blog/category/{slug}
+     */
+    public function category_show($slug)
+    {
+        try {
+            $category = Category::where('slug', $slug)->where('locale', $this->locale())->first();
+
+            if (! $category) {
+                return $this->error('category not found', 404);
+            }
+
+            $perPage = (int) request()->get('per_page', 12);
+
+            $blogs = $this->publishedBlogs()->where(function ($q) use ($category) {
+                $q->where('default_category', $category->id)
+                    ->orWhereRaw('FIND_IN_SET(?, categorys)', [$category->id]);
+            })->orderBy('id', 'DESC')->paginate($perPage);
+
+            return $this->success([
+                'category' => $category,
+                'items'    => $blogs->items(),
+                'meta'     => [
+                    'current_page' => $blogs->currentPage(),
+                    'last_page'    => $blogs->lastPage(),
+                    'per_page'     => $blogs->perPage(),
+                    'total'        => $blogs->total(),
+                ],
+            ], 'get category blogs successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+
+    /**
+     * 标签列表。
+     *
+     * GET /api/v1/blog/tag
+     */
+    public function tag_list()
+    {
+        try {
+            $tags = Tag::where('status', 1)
+                ->where('locale', $this->locale())
+                ->orderBy('id', 'DESC')
+                ->get();
+
+            return $this->success($tags, 'get blog tag successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+
+    /**
+     * 标签详情及其下博客列表。
+     *
+     * GET /api/v1/blog/tag/{slug}
+     */
+    public function tag_show($slug)
+    {
+        try {
+            $tag = Tag::where('slug', $slug)->where('locale', $this->locale())->first();
+
+            if (! $tag) {
+                return $this->error('tag not found', 404);
+            }
+
+            $perPage = (int) request()->get('per_page', 12);
+
+            $blogs = $this->publishedBlogs()
+                ->whereRaw('FIND_IN_SET(?, tags)', [$tag->id])
+                ->orderBy('id', 'DESC')
+                ->paginate($perPage);
+
+            return $this->success([
+                'tag'   => $tag,
+                'items' => $blogs->items(),
+                'meta'  => [
+                    'current_page' => $blogs->currentPage(),
+                    'last_page'    => $blogs->lastPage(),
+                    'per_page'     => $blogs->perPage(),
+                    'total'        => $blogs->total(),
+                ],
+            ], 'get tag blogs successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+
+    /**
+     * 作者博客列表。
+     *
+     * GET /api/v1/blog/author/{id}
+     */
+    public function author_blogs($authorId)
+    {
+        try {
+            $perPage = (int) request()->get('per_page', 12);
+
+            $blogs = $this->publishedBlogs()
+                ->where('author_id', $authorId)
+                ->orderBy('id', 'DESC')
+                ->paginate($perPage);
+
+            return $this->success([
+                'items' => $blogs->items(),
+                'meta'  => [
+                    'current_page' => $blogs->currentPage(),
+                    'last_page'    => $blogs->lastPage(),
+                    'per_page'     => $blogs->perPage(),
+                    'total'        => $blogs->total(),
+                ],
+            ], 'get author blogs successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+
+    /**
+     * 博客评论列表。
+     *
+     * GET /api/v1/blog/{id}/comments
+     */
+    public function comments($id)
+    {
+        try {
+            $blog = Blog::where('id', $id)->first();
+
+            if (! $blog) {
+                return $this->error('blog not found', 404);
+            }
+
+            $perPage = (int) request()->get('per_page', 20);
+
+            $comments = Comment::where('post', $blog->id)
+                ->where('status', 2)
+                ->where('parent_id', 0)
+                ->with('replies')
+                ->orderBy('id', 'DESC')
+                ->paginate($perPage);
+
+            return $this->success([
+                'items' => $comments->items(),
+                'meta'  => [
+                    'current_page' => $comments->currentPage(),
+                    'last_page'    => $comments->lastPage(),
+                    'per_page'     => $comments->perPage(),
+                    'total'        => $comments->total(),
+                ]
+            ], 'get blog comments successfully');
+        } catch (\Exception $e) {
+            return $this->error($e->getMessage(), 500);
+        }
+    }
+}

+ 71 - 0
packages/Longyi/Blog/src/Http/Controllers/Shop/ArticleController.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Shop;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Webkul\Theme\Repositories\ThemeCustomizationRepository;
+
+class ArticleController extends Controller
+{
+    use DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Using const variable for status
+     */
+    const STATUS = 1;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct(protected ThemeCustomizationRepository $themeCustomizationRepository)
+    {
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        $blogs = Blog::where('status', 1)->paginate(9);
+
+        $categories = Blog::groupBy('default_category')->selectRaw('default_category')->selectRaw('count(*) as count')->get();
+
+        $customizations = $this->themeCustomizationRepository->orderBy('sort_order')->findWhere([
+            'status'     => self::STATUS,
+            'channel_id' => core()->getCurrentChannel()->id
+        ]);
+
+        return view($this->_config['view'], compact('blogs', 'categories', 'customizations'));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  string  $slug
+     * @return \Illuminate\View\View
+     */
+    public function view($blog_slug, $slug)
+    {
+        $blog = Blog::where('slug', $slug)->firstOrFail();
+
+        $categories = Blog::groupBy('default_category')->selectRaw('default_category')->selectRaw('count(*) as count')->get();
+
+        return view($this->_config['view'], compact('blog', 'categories'));
+    }
+}

+ 221 - 0
packages/Longyi/Blog/src/Http/Controllers/Shop/BlogController.php

@@ -0,0 +1,221 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Shop;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Comment;
+use Webkul\Core\Models\CoreConfig;
+use Webkul\Theme\Repositories\ThemeCustomizationRepository;
+use Illuminate\Support\Facades\DB;
+
+class BlogController extends Controller
+{
+    use DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Using const variable for status
+     */
+    const STATUS = 1;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct(protected ThemeCustomizationRepository $themeCustomizationRepository)
+    {
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        $paginate = $this->getConfigByKey('blog_post_per_page');
+        $paginate = ( isset($paginate) && !empty($paginate) && is_null($paginate) ) ? (int)$paginate : 9;
+
+        $blogs = Blog::where('status', 1)->orderBy('id', 'desc')->paginate($paginate);
+
+        $categories = Category::where('status', 1)->get();
+
+        $tags = $this->getTagsWithCount();
+
+        $customizations = $this->themeCustomizationRepository->orderBy('sort_order')->findWhere([
+            'status'     => self::STATUS,
+            'channel_id' => core()->getCurrentChannel()->id
+        ]);
+
+        $show_categories_count = $this->getConfigByKey('blog_post_show_categories_with_count');
+        $show_tags_count = $this->getConfigByKey('blog_post_show_tags_with_count');
+        $show_author_page = $this->getConfigByKey('blog_post_show_author_page');
+
+        $blog_seo_meta_title = $this->getConfigByKey('blog_seo_meta_title');
+        $blog_seo_meta_keywords = $this->getConfigByKey('blog_seo_meta_keywords');
+        $blog_seo_meta_description = $this->getConfigByKey('blog_seo_meta_description');
+        return view($this->_config['view'], compact('blogs', 'categories', 'customizations', 'tags', 'show_categories_count', 'show_tags_count', 'show_author_page', 'blog_seo_meta_title', 'blog_seo_meta_keywords', 'blog_seo_meta_description'));
+    }
+
+    public function authorPage($author_id)
+    {
+        $show_author_page = $this->getConfigByKey('blog_post_show_author_page');
+        if ( (int)$show_author_page != 1 ) {
+            abort(404);
+        }
+
+        $author_data = Blog::where('author_id', $author_id)->firstOrFail();
+
+        $paginate = $this->getConfigByKey('blog_post_per_page');
+        $paginate = ( isset($paginate) && !empty($paginate) && is_null($paginate) ) ? (int)$paginate : 9;
+
+        $blogs = Blog::where('author_id', $author_id)->where('status', 1)->orderBy('id', 'desc')->paginate($paginate);
+
+        $categories = Category::where('status', 1)->get();
+
+        $tags = $this->getTagsWithCount();
+
+        $customizations = $this->themeCustomizationRepository->orderBy('sort_order')->findWhere([
+            'status'     => self::STATUS,
+            'channel_id' => core()->getCurrentChannel()->id
+        ]);
+
+        $show_categories_count = $this->getConfigByKey('blog_post_show_categories_with_count');
+        $show_tags_count = $this->getConfigByKey('blog_post_show_tags_with_count');
+        $show_author_page = $this->getConfigByKey('blog_post_show_author_page');
+
+        $blog_seo_meta_title = $this->getConfigByKey('blog_seo_meta_title');
+        $blog_seo_meta_keywords = $this->getConfigByKey('blog_seo_meta_keywords');
+        $blog_seo_meta_description = $this->getConfigByKey('blog_seo_meta_description');
+
+        return view($this->_config['view'], compact('blogs', 'categories', 'customizations', 'tags', 'author_data', 'show_categories_count', 'show_tags_count', 'show_author_page', 'blog_seo_meta_title', 'blog_seo_meta_keywords', 'blog_seo_meta_description'));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  string  $slug
+     * @return \Illuminate\View\View
+     */
+    public function view($blog_slug, $slug)
+    {
+        $blog = Blog::where('slug', $slug)->firstOrFail();
+
+        $blog_id = ( $blog && !empty($blog) && !is_null($blog) ) ? (int)$blog->id : 0;
+
+        $blog_tags = Tag::whereIn('id', explode(',',$blog->tags))->get();
+
+        $paginate = $this->getConfigByKey('blog_post_maximum_related');
+        $paginate = ( isset($paginate) && !empty($paginate) && is_null($paginate) ) ? (int)$paginate : 4;
+
+        $blog_category_ids = array_merge( explode(',', $blog->default_category), explode(',', $blog->categorys) );
+
+        $related_blogs = Blog::orderBy('id', 'desc')->where('status', 1)->whereNotIn('id', [$blog_id]);
+        if ( is_array($blog_category_ids) && !empty($blog_category_ids) && count($blog_category_ids) > 0 ) {
+            $related_blogs = $related_blogs->whereIn('default_category', $blog_category_ids)->where(
+            function ($query) use ($blog_category_ids) {
+                foreach ($blog_category_ids as $key => $blog_category_id) {
+                    if ( $key == 0 ) {
+                        $query->whereRaw('FIND_IN_SET(?, categorys)', [$blog_category_id]);
+                    } else {
+                        $query->orWhereRaw('FIND_IN_SET(?, categorys)', [$blog_category_id]);
+                    }
+                }
+            });
+        }
+        $related_blogs = $related_blogs->paginate($paginate);
+
+        $categories = Category::where('status', 1)->get();
+
+        $tags = $this->getTagsWithCount();
+
+        $comments = $this->getCommentsRecursive($blog_id);
+
+        $total_comments = Comment::where('post', $blog_id)->where('status', 2)->get();
+
+        $total_comments_cnt = ( !empty( $total_comments ) && count( $total_comments ) > 0 ) ? $total_comments->count() : 0;
+
+        $loggedIn_user_name = $loggedIn_user_email = null;
+        $loggedIn_user = auth()->guard('customer')->user();
+        if ( $loggedIn_user && isset($loggedIn_user) && !empty($loggedIn_user) && !is_null($loggedIn_user) ) {
+            $loggedIn_user_email = ( isset($loggedIn_user->email) && !empty($loggedIn_user->email) && !is_null($loggedIn_user->email) ) ? $loggedIn_user->email : null;
+            $loggedIn_user_first_name = ( isset($loggedIn_user->first_name) && !empty($loggedIn_user->first_name) && !is_null($loggedIn_user->first_name) ) ? $loggedIn_user->first_name : null;
+            $loggedIn_user_last_name = ( isset($loggedIn_user->last_name) && !empty($loggedIn_user->last_name) && !is_null($loggedIn_user->last_name) ) ? $loggedIn_user->last_name : null;
+            $loggedIn_user_name = $loggedIn_user_first_name;
+            $loggedIn_user_name = ( isset($loggedIn_user_name) && !empty($loggedIn_user_name) && !is_null($loggedIn_user_name) ) ? ( $loggedIn_user_name . ' ' . $loggedIn_user_last_name ) : $loggedIn_user_last_name;
+        }
+
+        $show_categories_count = $this->getConfigByKey('blog_post_show_categories_with_count');
+        $show_tags_count = $this->getConfigByKey('blog_post_show_tags_with_count');
+        $show_author_page = $this->getConfigByKey('blog_post_show_author_page');
+        $enable_comment = $this->getConfigByKey('blog_post_enable_comment');
+        $allow_guest_comment = $this->getConfigByKey('blog_post_allow_guest_comment');
+        $maximum_nested_comment = $this->getConfigByKey('blog_post_maximum_nested_comment');
+
+        $blog_seo_meta_title = $this->getConfigByKey('blog_seo_meta_title');
+        $blog_seo_meta_keywords = $this->getConfigByKey('blog_seo_meta_keywords');
+        $blog_seo_meta_description = $this->getConfigByKey('blog_seo_meta_description');
+
+        return view($this->_config['view'], compact('blog', 'categories', 'tags', 'comments', 'total_comments', 'total_comments_cnt', 'related_blogs', 'blog_tags', 'show_categories_count', 'show_tags_count', 'show_author_page', 'enable_comment', 'allow_guest_comment', 'maximum_nested_comment', 'loggedIn_user', 'loggedIn_user_name', 'loggedIn_user_email', 'blog_seo_meta_title', 'blog_seo_meta_keywords', 'blog_seo_meta_description'));
+    }
+
+    public function getTagsWithCount()
+    {
+        $blogTags = Blog::select('*')->get()->pluck('tags')->toarray();
+        $allBlogTags_arr = explode(',', implode(',', $blogTags));
+        $allBlogTags_arr = ( !empty($allBlogTags_arr) && count($allBlogTags_arr) > 0 ) ? $allBlogTags_arr : array();
+        $allBlogTags_arr_el_count = array_count_values($allBlogTags_arr);
+        $tags = Tag::where('status', 1)->get()->each(function ($item) use ($allBlogTags_arr, $allBlogTags_arr_el_count) {
+            $item->count = 0;
+            $tag_id = ( $item && isset($item->id) && !empty($item->id) && !is_null($item->id) ) ? (int)$item->id : 0;
+            if (count($allBlogTags_arr_el_count) > 0 && (int)$tag_id > 0) {
+                $item->count = ( array_key_exists($tag_id, $allBlogTags_arr_el_count) ) ? (int)$allBlogTags_arr_el_count[$tag_id] : 0;
+            }
+        });
+
+        return $tags;
+    }
+
+    public function getCommentsRecursive($blog_id = 0, $parent_id = 0)
+    {
+        $comments_datas = array();
+
+        $comments_details = Comment::where('post', $blog_id)->where('parent_id', $parent_id)->where('status', 2)->get();
+        if ( !empty($comments_details) && count($comments_details) > 0 ) {
+            $comments_datas = $comments_details->toarray();
+            if ( !empty($comments_datas) && count($comments_datas) > 0 ) {
+                foreach ($comments_datas as $key => $comments_data) {
+                    $comments_datas[$key]['replay'] = $this->getCommentsRecursive($blog_id, $comments_data['id']);
+                }
+            }
+        }
+
+        return $comments_datas;
+    }
+
+    public function getConfigByKey($code = '')
+    {
+        $config_val = null;
+        if ( isset($code) && !empty($code) && !is_null($code) ) {
+            $config = CoreConfig::where('code', $code)->first();
+            if ( $config ) {
+                $config_val = $config->value;
+            }
+        }
+        return $config_val;
+    }
+
+}

+ 83 - 0
packages/Longyi/Blog/src/Http/Controllers/Shop/CategoryController.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Shop;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Webkul\Core\Models\CoreConfig;
+use Webkul\Theme\Repositories\ThemeCustomizationRepository;
+use Illuminate\Support\Facades\DB;
+
+class CategoryController extends Controller
+{
+    use DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Using const variable for status
+     */
+    const STATUS = 1;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct(protected ThemeCustomizationRepository $themeCustomizationRepository)
+    {
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index($category_slug)
+    {
+        $category = Category::where('slug', $category_slug)->firstOrFail();
+
+        $category_id = ( $category && isset($category->id) ) ? $category->id : 0;
+
+        $paginate = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_post_per_page');
+        $paginate = ( isset($paginate) && !empty($paginate) && is_null($paginate) ) ? (int)$paginate : 9;
+
+        $blogs = Blog::orderBy('id', 'desc')->where('status', 1)
+        ->where(
+            function ($query) use ($category_id) {
+                $query->where('default_category', $category_id)
+                ->orWhereRaw('FIND_IN_SET(?, categorys)', [$category_id]);
+            })
+        ->paginate($paginate);
+
+        $categories = Category::where('status', 1)->get();
+
+        $tags = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getTagsWithCount();
+
+        $customizations = $this->themeCustomizationRepository->orderBy('sort_order')->findWhere([
+            'status'     => self::STATUS,
+            'channel_id' => core()->getCurrentChannel()->id
+        ]);
+
+        $show_categories_count = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_post_show_categories_with_count');
+        $show_tags_count = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_post_show_tags_with_count');
+        $show_author_page = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_post_show_author_page');
+
+        $blog_seo_meta_title = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_seo_meta_title');
+        $blog_seo_meta_keywords = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_seo_meta_keywords');
+        $blog_seo_meta_description = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_seo_meta_description');
+
+        return view($this->_config['view'], compact('blogs', 'categories', 'customizations', 'category', 'tags', 'show_categories_count', 'show_tags_count', 'show_author_page', 'blog_seo_meta_title', 'blog_seo_meta_keywords', 'blog_seo_meta_description'));
+    }
+    
+}

+ 87 - 0
packages/Longyi/Blog/src/Http/Controllers/Shop/CommentController.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Shop;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Longyi\Blog\Models\Comment;
+use Webkul\Core\Models\CoreConfig;
+use Longyi\Blog\Repositories\BlogCommentRepository;
+use Webkul\Theme\Repositories\ThemeCustomizationRepository;
+use Illuminate\Support\Facades\DB;
+
+class CommentController extends Controller
+{
+    use DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Using const variable for status
+     */
+    const STATUS = 1;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct
+    (
+    	protected ThemeCustomizationRepository $themeCustomizationRepository,
+    	protected BlogCommentRepository $blogCommentRepository,
+    )
+    {
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Store a newly created comment.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function store()
+    {
+        try {
+            $data = request()->all();
+
+            // 评论默认待审核(status=1),管理员审核通过后置为 status=2
+            $data['status'] = 1;
+
+            $result = $this->blogCommentRepository->save($data);
+
+            // 若是 API 请求(无 _config 视图配置),返回 JSON
+            if (request()->expectsJson() || request()->is('api/*')) {
+                return response()->json([
+                    'status_code' => 200,
+                    'status'      => 'success',
+                    'message'     => 'Your comment has been created successfully.',
+                    'data'        => null,
+                ], 200);
+            }
+
+            return redirect()->back()->with('success', 'Your comment has been created successfully.');
+        } catch (\Exception $e) {
+            if (request()->expectsJson() || request()->is('api/*')) {
+                return response()->json([
+                    'status_code' => 500,
+                    'status'      => 'error',
+                    'message'     => $e->getMessage(),
+                    'data'        => null,
+                ], 500);
+            }
+
+            return redirect()->back()->with('error', $e->getMessage());
+        }
+    }
+
+}

+ 77 - 0
packages/Longyi/Blog/src/Http/Controllers/Shop/TagController.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace Longyi\Blog\Http\Controllers\Shop;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Webkul\Core\Models\CoreConfig;
+use Webkul\Theme\Repositories\ThemeCustomizationRepository;
+use Illuminate\Support\Facades\DB;
+
+class TagController extends Controller
+{
+    use DispatchesJobs, ValidatesRequests;
+
+    /**
+     * Contains route related configuration
+     *
+     * @var array
+     */
+    protected $_config;
+
+    /**
+     * Using const variable for status
+     */
+    const STATUS = 1;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct(protected ThemeCustomizationRepository $themeCustomizationRepository)
+    {
+        $this->_config = request('_config');
+    }
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index($tag_slug)
+    {
+        $tag = Tag::where('slug', $tag_slug)->firstOrFail();
+
+        $tag_id = ( $tag && isset($tag->id) ) ? $tag->id : 0;
+
+        $paginate = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_post_per_page');
+        $paginate = ( isset($paginate) && !empty($paginate) && is_null($paginate) ) ? (int)$paginate : 9;
+
+        $blogs = Blog::orderBy('id', 'desc')->where('status', 1)->whereRaw('FIND_IN_SET(?, tags)', [$tag_id])->paginate($paginate);
+
+        $categories = Category::where('status', 1)->get();
+
+        $tags = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getTagsWithCount();
+
+        $customizations = $this->themeCustomizationRepository->orderBy('sort_order')->findWhere([
+            'status'     => self::STATUS,
+            'channel_id' => core()->getCurrentChannel()->id
+        ]);
+
+        $show_categories_count = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_post_show_categories_with_count');
+        $show_tags_count = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_post_show_tags_with_count');
+        $show_author_page = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_post_show_author_page');
+
+        $blog_seo_meta_title = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_seo_meta_title');
+        $blog_seo_meta_keywords = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_seo_meta_keywords');
+        $blog_seo_meta_description = app('Longyi\Blog\Http\Controllers\Shop\BlogController')->getConfigByKey('blog_seo_meta_description');
+
+        return view($this->_config['view'], compact('blogs', 'categories', 'customizations', 'tag', 'tags', 'show_categories_count', 'show_tags_count', 'show_author_page', 'blog_seo_meta_title', 'blog_seo_meta_keywords', 'blog_seo_meta_description'));
+    }
+    
+}

+ 52 - 0
packages/Longyi/Blog/src/Http/Requests/BlogCategoryRequest.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Longyi\Blog\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Longyi\Blog\Validations\BlogCategoryUniqueSlug;
+
+class BlogCategoryRequest extends FormRequest
+{
+    /**
+     * Determine if the Configuration is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        $locale = core()->getRequestedLocaleCode();
+        $locale = ( is_array( $locale ) && count( $locale ) > 0 ) ? $locale[0] : 'en';
+
+        if ($id = request('id')) {
+            return [
+                'slug'              => ['required', new BlogCategoryUniqueSlug('blog_categories', $id)],
+                'name'              => 'required',
+                'image.*'           => 'mimes:bmp,jpeg,jpg,png,webp',
+                'description'       => 'required',
+                'meta_title'        => 'required',
+                'meta_description'  => 'required',
+                'meta_keywords'     => 'required',
+            ];
+        }
+
+        return [
+            'slug'              => ['required', new BlogCategoryUniqueSlug],
+            'name'              => 'required',
+            'image.*'           => 'mimes:bmp,jpeg,jpg,png,webp',
+            'description'       => 'required',
+            'meta_title'        => 'required',
+            'meta_description'  => 'required',
+            'meta_keywords'     => 'required',
+        ];
+    }
+}

+ 64 - 0
packages/Longyi/Blog/src/Http/Requests/BlogRequest.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace Longyi\Blog\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Longyi\Blog\Validations\BlogUniqueSlug;
+
+class BlogRequest extends FormRequest
+{
+    /**
+     * Determine if the Configuration is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        $locale = core()->getRequestedLocaleCode();
+        $locale = ( is_array( $locale ) && count( $locale ) > 0 ) ? $locale[0] : 'en';
+
+        if ($id = request('id')) {
+            return [
+                'slug'              => ['required', new BlogUniqueSlug('blogs', $id)],
+                'name'              => 'required',
+                'src.*'             => 'mimes:bmp,jpeg,jpg,png,webp',
+                'm_src.*'           => 'mimes:bmp,jpeg,jpg,png,webp',
+                'short_description' => 'required',
+                'description'       => 'required',
+                'default_category'  => 'required',
+                'tags.*'            => 'required',
+                'author_id'         => 'required',
+                'meta_title'        => 'required',
+                'meta_description'  => 'required',
+                'meta_keywords'     => 'required',
+                'published_at'      => 'required',
+            ];
+        }
+
+        return [
+            'slug'              => ['required', new BlogUniqueSlug],
+            'name'              => 'required',
+            'src.*'             => 'mimes:bmp,jpeg,jpg,png,webp',
+            'm_src.*'           => 'mimes:bmp,jpeg,jpg,png,webp',
+            'short_description' => 'required',
+            'description'       => 'required',
+            'meta_title'        => 'required',
+            'meta_description'  => 'required',
+            'meta_keywords'     => 'required',
+            'default_category'  => 'required',
+            'tags.*'            => 'required',
+            'author_id'         => 'required',
+            'published_at'      => 'required',
+        ];
+    }
+}

+ 52 - 0
packages/Longyi/Blog/src/Http/Requests/BlogTagRequest.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Longyi\Blog\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Longyi\Blog\Validations\BlogTagUniqueSlug;
+
+class BlogTagRequest extends FormRequest
+{
+    /**
+     * Determine if the Configuration is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        $locale = core()->getRequestedLocaleCode();
+        $locale = ( is_array( $locale ) && count( $locale ) > 0 ) ? $locale[0] : 'en';
+
+        if ($id = request('id')) {
+            return [
+                'slug'              => ['required', new BlogTagUniqueSlug('blog_tags', $id)],
+                'name'              => 'required',
+                'description'       => 'required',
+                'image.*'           => 'mimes:bmp,jpeg,jpg,png,webp',
+                'meta_title'        => 'required',
+                'meta_description'  => 'required',
+                'meta_keywords'     => 'required',
+            ];
+        }
+
+        return [
+            'slug'              => ['required', new BlogTagUniqueSlug],
+            'name'              => 'required',
+            'description'       => 'required',
+            'image.*'           => 'mimes:bmp,jpeg,jpg,png,webp',
+            'meta_title'        => 'required',
+            'meta_description'  => 'required',
+            'meta_keywords'     => 'required',
+        ];
+    }
+}

+ 322 - 0
packages/Longyi/Blog/src/Import/BlogImport.php

@@ -0,0 +1,322 @@
+<?php
+
+namespace Longyi\Blog\Import;
+
+use Longyi\Blog\Models\Blog;
+use Longyi\Blog\Models\Category;
+use Longyi\Blog\Models\Tag;
+use Webkul\User\Models\Admin;
+use Maatwebsite\Excel\Concerns\ToModel;
+use Maatwebsite\Excel\Concerns\WithHeadingRow;
+use Illuminate\Support\Facades\Auth;
+use DB;
+use Session;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Arr;
+use Maatwebsite\Excel\Concerns\ToArray;
+use Maatwebsite\Excel\Concerns\ToCollection;
+use Maatwebsite\Excel\Concerns\RemembersRowNumber;
+use Maatwebsite\Excel\Concerns\WithChunkReading;
+use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+
+class BlogImport implements ToModel, WithHeadingRow
+{
+    use RemembersRowNumber;
+
+    function __construct()
+    {
+        //
+    }
+    
+    public function model(array $row)
+    {
+        $import_flag = $this->validation_import_blogs( $row );
+        if ( $import_flag == true ) {
+
+            $category_slugs = array_key_exists( 'category_slugs', $row ) 
+                                ? array_values( array_filter( array_map( function( $val ) { return str_replace( ' ', '_', trim( $val ) ); }, array_values( array_unique( array_filter( explode( ',', $row[ 'category_slugs' ] ) ) ) ) ) ) )
+                                : array();
+            $cat_ids = [];
+            if ( $category_slugs ) {
+                $if_cat_exist = Category::whereIn( 'slug', $category_slugs )->get();
+                if ( $if_cat_exist && count( $if_cat_exist ) > 0 ) {
+                    $cat_ids = $if_cat_exist->pluck( 'id' )->unique()->toarray();
+                }
+            }
+
+            $tag_slugs = array_key_exists( 'tag_slugs', $row ) 
+                                ? array_values( array_filter( array_map( function( $val ) { return str_replace( ' ', '_', trim( $val ) ); }, array_values( array_unique( array_filter( explode( ',', $row[ 'tag_slugs' ] ) ) ) ) ) ) )
+                                : array();
+            $tag_ids = [];
+            if ( $category_slugs ) {
+                $if_tag_exist = Tag::whereIn( 'slug', $tag_slugs )->get();
+                if ( $if_tag_exist && count( $if_tag_exist ) > 0 ) {
+                    $tag_ids = $if_tag_exist->pluck( 'id' )->unique()->toarray();
+                }
+            }
+
+            $author = null;
+            $author_id = 0;
+            if ( is_array( $row ) && array_key_exists( 'author_email', $row ) && isset( $row[ 'author_email' ] ) && !empty( $row[ 'author_email' ] ) && !is_null( $row[ 'author_email' ] ) ) {
+                $if_author_exist = Admin::where( 'email', $row[ 'author_email' ] )->first();
+                if ( $if_author_exist ) {
+                    $author = $if_author_exist->name;
+                    $author_id = $if_author_exist->id;
+                }
+            }
+
+            $import_data = array(
+                'name' => isset($row['name']) && $row['name'] != '' ? $row['name'] : '',
+                'slug' => isset($row['slug']) && $row['slug'] != '' ? $row['slug'] : '',
+                'short_description' => isset($row['short_description']) && $row['short_description'] != '' ? $row['short_description'] : '',
+                'description' => isset($row['description']) && $row['description'] != '' ? $row['description'] : '',
+                'channels' => 1,
+                'default_category' => ( is_array( $cat_ids ) && count( $cat_ids ) > 0 ) ? $cat_ids[0] : 0,
+                'categorys' => ( is_array( $cat_ids ) && count( $cat_ids ) > 0 ) ? implode( ',', $cat_ids ) : '',
+                'tags' => ( is_array( $tag_ids ) && count( $tag_ids ) > 0 ) ? implode( ',', $tag_ids ) : '',
+                'author' => $author,
+                'author_id' => $author_id,
+                'src' => '',
+                'status' => isset($row['status']) && $row['status'] != '' && $row['status'] == 1 ? 1 : 0,
+                'locale' => core()->getRequestedLocaleCode(),
+                'allow_comments' => isset($row['allow_comments']) && $row['allow_comments'] != '' && $row['allow_comments'] == 1 ? 1 : 0,
+                'meta_title' => isset($row['meta_title']) && $row['meta_title'] != '' ? $row['meta_title'] : '',
+                'meta_description' => isset($row['meta_description']) && $row['meta_description'] != '' ? $row['meta_description'] : '',
+                'meta_keywords' => isset($row['meta_keywords']) && $row['meta_keywords'] != '' ? $row['meta_keywords'] : '',
+                'published_at' => isset($row['published_at']) && $row['published_at'] != '' ? $row['published_at'] : '',
+            );
+
+            $if_exist = Blog::where('slug', $row['slug'])->first();
+            if ($if_exist) {
+                unset( $import_data[ 'slug' ] );
+                $import_data[ 'src' ] = $this->check_and_upload_file( $row, $if_exist->id );
+                Blog::where('slug', $row['slug'])->update( $import_data );
+            } else {
+                $store_blog = Blog::create( $import_data );
+                if ( $store_blog ) {
+                    $blog_src = $this->check_and_upload_file( $row, $store_blog->id );
+                    Blog::where('id', $store_blog->id )->update([ 'src' => $blog_src ]);
+                }
+            }
+
+        }
+    }
+
+    public function validation_import_blogs( $row = [] )
+    {
+        $check_data_flag = true;
+        if ( !empty( $row ) && count( $row ) > 0 ) {
+            $err_data = $messages = [];
+            $check_data_flag = true;
+            $validation_arr = array(
+                'name' => 'required',
+                'slug' => 'required',
+                'short_description' => 'required',
+                'description' => 'required',
+                'category_slugs' => 'required',
+                'tag_slugs' => 'required',
+                'author_email' => 'required|email:rfc,dns',
+                // 'image_url' => 'required',
+                'status' => 'required|numeric',
+                'allow_comments' => 'required|numeric',
+                'meta_title' => 'required',
+                'meta_description' => 'required',
+                'meta_keywords' => 'required',
+                'published_at' => 'required|date',
+            );
+            
+            $row_number = $this->getRowNumber();
+
+            $validator = Validator::make($row, $validation_arr);
+            if ($validator->fails()){
+                $message = $validator->errors()->all();
+                $messages = $message;
+                $check_data_flag = false;
+            }
+
+            if ( is_array( $row ) && array_key_exists( 'category_slugs', $row ) && isset( $row[ 'category_slugs' ] ) && !empty( $row[ 'category_slugs' ] ) && !is_null( $row[ 'category_slugs' ] ) ) {
+                $check_blog_category_slugs = $this->check_blog_category_slugs( $row );
+                if ( is_array( $check_blog_category_slugs ) ) {
+                    if ( array_key_exists( 'check_data_flag', $check_blog_category_slugs ) ) {
+                        $check_data_flag = $check_blog_category_slugs[ 'check_data_flag' ];
+                    }
+                    if ( array_key_exists( 'messages', $check_blog_category_slugs ) ) {
+                        $messages[] = $check_blog_category_slugs[ 'messages' ];
+                    }
+                }
+            }
+
+            if ( is_array( $row ) && array_key_exists( 'tag_slugs', $row ) && isset( $row[ 'tag_slugs' ] ) && !empty( $row[ 'tag_slugs' ] ) && !is_null( $row[ 'tag_slugs' ] ) ) {
+                $check_blog_tag_slugs = $this->check_blog_tag_slugs( $row );
+                if ( is_array( $check_blog_tag_slugs ) ) {
+                    if ( array_key_exists( 'check_data_flag', $check_blog_tag_slugs ) ) {
+                        $check_data_flag = $check_blog_tag_slugs[ 'check_data_flag' ];
+                    }
+                    if ( array_key_exists( 'messages', $check_blog_tag_slugs ) ) {
+                        $messages[] = $check_blog_tag_slugs[ 'messages' ];
+                    }
+                }
+            }
+
+            if ( is_array( $row ) && array_key_exists( 'author_email', $row ) && isset( $row[ 'author_email' ] ) && !empty( $row[ 'author_email' ] ) && !is_null( $row[ 'author_email' ] ) ) {
+                $check_author_email = $this->check_author_email( $row );
+                if ( is_array( $check_author_email ) ) {
+                    if ( array_key_exists( 'check_data_flag', $check_author_email ) ) {
+                        $check_data_flag = $check_author_email[ 'check_data_flag' ];
+                    }
+                    if ( array_key_exists( 'messages', $check_author_email ) ) {
+                        $messages[] = $check_author_email[ 'messages' ];
+                    }
+                }
+            }
+
+            $check_image_url = $this->check_and_upload_file( $row, 0, 'check' );
+            if ( isset( $check_image_url ) && !empty( $check_image_url ) && !is_null( $check_image_url ) ) {
+                $check_data_flag = false;
+                $messages[] = $check_image_url;
+            }
+
+            $blog_name = array_key_exists( 'name', $row ) ? $row["name"] : null;
+            $blog_name = trim( $blog_name );
+            $line_no = $row_number . ( ( isset( $blog_name ) && !empty( $blog_name ) ) ? ' [ ' . $blog_name . ' ]' : null );
+
+            if ( $check_data_flag == false ) {
+                if ( Session::has( 'import_errors' ) ) {
+                    $err_data = Session::get( 'import_errors' );
+                }
+                if ( $check_data_flag == false ) {
+                    $err_data[] = array( 'line_no' => $line_no, 'errors' => $messages );
+                    // null row data is not error
+                    if( array_filter( $row ) ) {
+                        Session::put( 'import_errors', $err_data );
+                    } 
+                }
+            }
+
+        }
+        return $check_data_flag;
+    }
+
+    public function check_blog_category_slugs( $row = [] )
+    {
+        $category_ids = array();
+        $check_data_flag = false;
+        $messages = 'invalid category slugs data';
+        if ( is_array( $row ) && count( $row ) > 0 ) {
+            $category_slugs = array_key_exists( 'category_slugs', $row ) ? array_values( array_unique( array_filter( explode( ',', $row[ 'category_slugs' ] ) ) ) ) : array();
+            if ( is_array( $category_slugs ) && count( $category_slugs ) > 0 ) {
+                foreach ( $category_slugs as $category ) {
+                    $category_slug = str_replace( ' ', '_', trim( $category ) );
+                    if ( isset( $category_slug ) && !empty( $category_slug ) && !is_null( $category_slug ) ) {
+                        $if_exist = Category::where( 'slug', $category_slug )->first();
+                        if ( $if_exist ) {
+                            $category_ids[] = $if_exist->id;
+                        } else {
+                            $cat_store = Category::create([
+                                'name' => ucwords( strtolower( str_replace( "_", " ", $category_slug ) ) ),
+                                'slug' => $category_slug,
+                                'description' => strtolower( str_replace( "_", " ", $category_slug ) ),
+                                'image' => '',
+                                'status' => 1,
+                                'parent_id' => null,
+                                'locale' => core()->getRequestedLocaleCode(),
+                                'meta_title' => ucwords( strtolower( str_replace( "_", " ", $category_slug ) ) ),
+                                'meta_description' => strtolower( str_replace( "_", " ", $category_slug ) ),
+                                'meta_keywords' => strtolower( str_replace( "_", " ", $category_slug ) ),
+                            ]);
+                            if ( $cat_store ) {
+                                $category_ids[] = $cat_store->id;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        if ( is_array( $category_ids ) && count( $category_ids ) > 0 ) {
+            $check_data_flag = true;
+            $messages = '';
+        }
+        return array( 'check_data_flag' => $check_data_flag, 'messages' => $messages );
+    }
+
+    public function check_blog_tag_slugs( $row = [] )
+    {
+        $tag_ids = array();
+        $check_data_flag = false;
+        $messages = 'invalid tag slugs data';
+        if ( is_array( $row ) && count( $row ) > 0 ) {
+            $tag_slugs = array_key_exists( 'tag_slugs', $row ) ? array_values( array_unique( array_filter( explode( ',', $row[ 'tag_slugs' ] ) ) ) ) : array();
+            if ( is_array( $tag_slugs ) && count( $tag_slugs ) > 0 ) {
+                foreach ( $tag_slugs as $tag_slug ) {
+                    $tag_slug = str_replace( ' ', '_', trim( $tag_slug ) );
+                    if ( isset( $tag_slug ) && !empty( $tag_slug ) && !is_null( $tag_slug ) ) {
+                        $if_exist = Tag::where( 'slug', $tag_slug )->first();
+                        if ( $if_exist ) {
+                            $tag_ids[] = $if_exist->id;
+                        } else {
+                            $tag_store = Tag::create([
+                                'name' => ucwords( strtolower( str_replace( "_", " ", $tag_slug ) ) ),
+                                'slug' => $tag_slug,
+                                'description' => strtolower( str_replace( "_", " ", $tag_slug ) ),
+                                'status' => 1,
+                                'locale' => core()->getRequestedLocaleCode(),
+                                'meta_title' => ucwords( strtolower( str_replace( "_", " ", $tag_slug ) ) ),
+                                'meta_description' => strtolower( str_replace( "_", " ", $tag_slug ) ),
+                                'meta_keywords' => strtolower( str_replace( "_", " ", $tag_slug ) ),
+                            ]);
+                            if ( $tag_store ) {
+                                $tag_ids[] = $tag_store->id;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        if ( is_array( $tag_ids ) && count( $tag_ids ) > 0 ) {
+            $check_data_flag = true;
+            $messages = '';
+        }
+        return array( 'check_data_flag' => $check_data_flag, 'messages' => $messages );
+    }
+
+    public function check_author_email( $row = [] )
+    {
+        $check_data_flag = false;
+        $messages = 'invalid author email';
+        if ( is_array( $row ) && array_key_exists( 'author_email', $row ) && isset( $row[ 'author_email' ] ) && !empty( $row[ 'author_email' ] ) && !is_null( $row[ 'author_email' ] ) ) {
+            $if_author_exist = Admin::where( 'email', $row[ 'author_email' ] )->first();
+            if ( $if_author_exist ) {
+                $check_data_flag = true;
+                $messages = '';
+            }
+        }
+        return array( 'check_data_flag' => $check_data_flag, 'messages' => $messages );
+    }
+
+    public function check_and_upload_file( $row = [], $blog_id = 0, $operation = 'upload', $type = 'image_url' )
+    {
+        $file_path = '';
+        $file_flag = false;
+        if ( isset( $type ) && !empty( $type ) && !is_null( $type ) && is_array( $row ) && array_key_exists( $type, $row ) && isset( $row[ $type ] ) && !empty( $row[ $type ] ) && !is_null( $row[ $type ] ) ) {
+
+            $file_url = $row[ $type ];
+            $file_name = basename( $file_url );
+            $file_contents = file_get_contents($file_url);
+            if ( strlen($file_contents) > 0 ) {
+                if ( $operation == 'upload' ) {
+                    $file_path = 'blog-images/' . (int)$blog_id . '/' . $file_name;
+                    Storage::put( $file_path, $file_contents );
+                }
+                $file_flag = true;
+            }
+            
+            if ( $file_flag == false ) {
+                $file_path = ( $operation == 'upload' ) ? '' : 'invalid image url';
+            }
+        }
+        return $file_path;
+    }
+
+}

+ 102 - 0
packages/Longyi/Blog/src/Models/Blog.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace Longyi\Blog\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Longyi\Blog\Contracts\Blog as BlogContract;
+use Webkul\Core\Models\ChannelProxy;
+use Illuminate\Support\Facades\Storage;
+use Longyi\Blog\Models\Category;
+
+class Blog extends Model implements BlogContract
+{
+    use HasFactory;
+
+    protected $table = 'blogs';
+
+    protected $fillable = [
+        'name',
+        'slug',
+        'short_description',
+        'description',
+        'channels',
+        'default_category',
+        'author',
+        'author_id',
+        'categorys',
+        'tags',
+        'src',
+        'm_src',
+        'status',
+        'locale',
+        'allow_comments',
+        'meta_title',
+        'meta_description',
+        'meta_keywords',
+        'published_at'
+    ];
+
+    /**
+     * Appends.
+     *
+     * @var array
+     */
+    protected $appends = ['src_url', 'm_src_url', 'assign_categorys'];
+
+    /**
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function category()
+    {
+        return $this->belongsTo(Category::class, 'default_category');
+    }
+
+    /**
+     * Get the channels.
+     */
+    public function channels()
+    {
+        return $this->belongsToMany(ChannelProxy::modelClass(), 'channels');
+    }
+
+    /**
+     * Get image url for the category image.
+     *
+     * @return string
+     */
+    public function getSrcUrlAttribute()
+    {
+        if (! $this->src) {
+            return;
+        }
+
+        return Storage::url($this->src);
+    }
+
+    /**
+     * Get image url for the m_src (mobile) image.
+     *
+     * @return string
+     */
+    public function getMSrcUrlAttribute()
+    {
+        if (! $this->m_src) {
+            return;
+        }
+
+        return Storage::url($this->m_src);
+    }
+
+    public function getAssignCategorysAttribute()
+    {
+        $categorys = array();
+        $categories_ids = array_values( array_unique( array_merge( explode( ',', $this->default_category ), explode( ',', $this->categorys ) ) ) );
+        if ( is_array($categories_ids) && !empty($categories_ids) && count($categories_ids) > 0 ) {
+            $categories = Category::whereIn('id', $categories_ids)->get();
+            $categorys = ( !empty($categories) && count($categories) > 0 ) ? $categories : array();
+        }
+        return $categorys;
+    }
+
+}

+ 138 - 0
packages/Longyi/Blog/src/Models/Category.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace Longyi\Blog\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Longyi\Blog\Contracts\Category as CategoryContract;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Session;
+
+class Category extends Model implements CategoryContract
+{
+    use HasFactory;
+
+    protected $table = 'blog_categories';
+
+    protected $fillable = [
+        'name',
+        'slug',
+        'description',
+        'image',
+        'status',
+        'parent_id',
+        'locale',
+        'meta_title',
+        'meta_description',
+        'meta_keywords',
+        'created_at',
+        'updated_at'
+    ];
+
+    /**
+     * Appends.
+     *
+     * @var array
+     */
+    protected $appends = ['image_url', 'parent_category_name', 'children', 'assign_blogs'];
+
+    public function blog()
+    {
+        return $this->hasMany(Blog::class, 'default_category');
+    }
+
+    /**
+     * Get image url for the category image.
+     *
+     * @return string
+     */
+    public function getImageUrlAttribute()
+    {
+        if (! $this->image) {
+            return;
+        }
+
+        return Storage::url($this->image);
+    }
+
+    /**
+     * Get parent category name for the category.
+     *
+     * @return string
+     */
+    public function getParentCategoryNameAttribute()
+    {
+        if (! $this->parent_id || (int)$this->parent_id <= 0) {
+            return;
+        }
+
+        $category = Category::find($this->parent_id);
+
+        $parent_category_name = $category ? $category->name : null;
+
+        return $parent_category_name;
+    }
+
+    /**
+     * Get parent category for the category.
+     *
+     * @return string
+     */
+    public function getParentCategoryAttribute()
+    {
+        if (! $this->parent_id || (int)$this->parent_id <= 0) {
+            return;
+        }
+
+        return Category::find($this->parent_id);
+    }
+
+    /**
+     * Get child category for the category.
+     *
+     * @return string
+     */
+    public function getChildrenAttribute()
+    {
+        if (! $this->id || (int)$this->id <= 0) {
+            return;
+        }
+
+        if ( Session::has('bCatEditId') && Session::get('bCatEditId') > 0 ) {
+            return Category::where('id', '!=', Session::get('bCatEditId'))->where('parent_id', $this->id)->get();
+        }
+
+        return Category::where('parent_id', $this->id)->get();
+    }
+
+    /**
+     * Get child category for the category.
+     *
+     * @return string
+     */
+    public function getAssignBlogsAttribute()
+    {
+        if (! $this->id || (int)$this->id <= 0) {
+            return 0;
+        }
+
+        $assign_blogs = 0;
+
+        $this_id = $this->id;
+
+        $blogs = Blog::where('status', 1)
+        ->where(
+            function ($query) use ($this_id) {
+                $query->where('default_category', $this_id)
+                ->orWhereRaw('FIND_IN_SET(?, categorys)', [$this_id]);
+            })
+        ->get();
+
+        if ( !empty($blogs) && count($blogs) > 0 ) {
+            $assign_blogs = count($blogs);
+        }
+
+        return $assign_blogs;
+    }
+
+}

+ 42 - 0
packages/Longyi/Blog/src/Models/Comment.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace Longyi\Blog\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Longyi\Blog\Contracts\Comment as CommentContract;
+
+class Comment extends Model implements CommentContract
+{
+    use HasFactory;
+
+    protected $table = 'blog_comments';
+
+    protected $fillable = [
+        'post',
+        'author',
+        'name',
+        'email',
+        'comment',
+        'parent_id',
+        'status'
+    ];
+
+    /**
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function blog()
+    {
+        return $this->hasOne('Longyi\Blog\Models\Blog', 'id', 'post');
+    }
+
+    /**
+     * 评论的回复(子评论)。
+     *
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function replies()
+    {
+        return $this->hasMany(Comment::class, 'parent_id', 'id');
+    }
+}

+ 26 - 0
packages/Longyi/Blog/src/Models/Tag.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Longyi\Blog\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Longyi\Blog\Contracts\Tag as TagContract;
+
+class Tag extends Model implements TagContract
+{
+    use HasFactory;
+
+    protected $table = 'blog_tags';
+
+    protected $fillable = [
+        'name',
+        'slug',
+        'description',
+        'image',
+        'status',
+        'locale',
+        'meta_title',
+        'meta_description',
+        'meta_keywords'
+    ];
+}

+ 70 - 0
packages/Longyi/Blog/src/Providers/BlogServiceProvider.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace Longyi\Blog\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Routing\Router;
+
+class BlogServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap services.
+     *
+     * @return void
+     */
+    public function boot(Router $router)
+    {
+
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/shop-routes.php');
+
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'blog');
+
+        $this->publishes([
+            __DIR__ . '/../../publishable/assets' => public_path('themes/default/assets'),
+        ], 'public');
+
+        $this->publishes([
+            __DIR__ . '/../../publishable/blog' => storage_path('app/public'),
+        ], 'public');
+
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'blog');
+
+    }
+    
+    /**
+     * Register services.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        $this->registerConfig();
+    }
+    
+    /**
+     * Register package config.
+     *
+     * @return void
+     */
+    protected function registerConfig()
+    {
+
+        // $this->mergeConfigFrom(
+        //     dirname(__DIR__) . '/Config/l5-swagger.php', 'bagisto-rest-api-swagger'
+        // );
+
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/menu.php', 'menu.admin'
+        );
+
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/acl.php', 'acl'
+        );
+
+    }
+}

+ 15 - 0
packages/Longyi/Blog/src/Providers/ModuleServiceProvider.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Longyi\Blog\Providers;
+
+use Konekt\Concord\BaseModuleServiceProvider;
+
+class ModuleServiceProvider extends BaseModuleServiceProvider
+{
+    protected $models = [
+        \Longyi\Blog\Models\Blog::class,
+        \Longyi\Blog\Models\Category::class,
+        \Longyi\Blog\Models\Tag::class,
+        \Longyi\Blog\Models\Comment::class,
+    ];
+}

+ 150 - 0
packages/Longyi/Blog/src/Repositories/BlogCategoryRepository.php

@@ -0,0 +1,150 @@
+<?php
+
+namespace Longyi\Blog\Repositories;
+
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\DB;
+use Webkul\Core\Eloquent\Repository;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Storage;
+use Intervention\Image\ImageManager;
+use Illuminate\Support\Str;
+
+class BlogCategoryRepository extends Repository
+{
+    /**
+     * Specify Model class name
+     *
+     * @return mixed
+     */
+    function model()
+    {
+        return 'Longyi\Blog\Models\Category';
+    }
+
+    /**
+     * Save blog category.
+     *
+     * @param  array  $data
+     * @return bool|\Longyi\Blog\Contracts\Category
+     */
+    public function save(array $data)
+    {
+        Event::dispatch('admin.blog.categories.create.before', $data);
+
+        $create_data = $data;
+
+        if ( array_key_exists('image', $create_data) ) {
+            unset($create_data['image']);
+        }
+
+        $categories = $this->create($create_data);
+
+        $this->uploadImages($data, $categories);
+
+        Event::dispatch('admin.blog.categories.create.after', $categories);
+
+        return true;
+    }
+
+    /**
+     * Update item.
+     *
+     * @param  array  $data
+     * @param  int  $id
+     * @return bool
+     */
+    public function updateItem(array $data, $id)
+    {
+        Event::dispatch('admin.blog.categories.update.before', $id);
+
+        $update_data = $data;
+
+        if ( array_key_exists('image', $update_data) ) {
+            unset($update_data['image']);
+        }
+
+        $categories = $this->update($update_data, $id);
+
+        $this->uploadImages($data, $categories);
+
+        Event::dispatch('admin.blog.categories.update.after', $categories);
+
+        return true;
+    }
+
+    /**
+     * Upload category's images.
+     *
+     * @param  array  $data
+     * @param  \Webkul\Category\Contracts\Category  $category
+     * @param  string  $type
+     * @return void
+     */
+    public function uploadImages($data, $category, $type = 'image')
+    {
+        if (isset($data[$type])) {
+            foreach ($data[$type] as $imageId => $image) {
+                $file = $type . '.' . $imageId;
+
+                $dir = 'blog-category/' . $category->id;
+
+                if (request()->hasFile($file)) {
+                    if ($category->{$type}) {
+                        Storage::delete($category->{$type});
+                    }
+
+                    $manager = new ImageManager();
+
+                    $image = $manager->make(request()->file($file))->encode('webp');
+
+                    $category->{$type} = 'blog-category/' . $category->id . '/' . Str::random(40) . '.webp';
+
+                    Storage::put($category->{$type}, $image);
+
+                    $category->save();
+                }
+            }
+        } else {
+            if ($category->{$type}) {
+                Storage::delete($category->{$type});
+            }
+
+            $category->{$type} = null;
+
+            $category->save();
+        }
+    }
+
+    /**
+     * Delete a blog category item and delete the image from the disk or where ever it is.
+     *
+     * @param  int  $id
+     * @return bool
+     */
+    public function destroy($id)
+    {
+        $categoryItem = $this->find($id);
+
+        $categoryItemImage = $categoryItem->image;
+
+        Storage::delete($categoryItemImage);
+
+        return $this->model->destroy($id);
+    }
+
+    /**
+     * Get only active blog categories.
+     *
+     * @return array
+     */
+    public function getActiveBlogCategories()
+    {
+        $currentLocale = core()->getCurrentLocale();
+
+        return $this->whereRaw("find_in_set(?, locale)", [$currentLocale->code])
+            ->orderBy('sort_order', 'ASC')
+            ->get()
+            ->toArray();
+    }
+}

+ 84 - 0
packages/Longyi/Blog/src/Repositories/BlogCommentRepository.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace Longyi\Blog\Repositories;
+
+use Illuminate\Support\Facades\Event;
+use Webkul\Core\Eloquent\Repository;
+
+class BlogCommentRepository extends Repository
+{
+    /**
+     * Specify Model class name
+     *
+     * @return mixed
+     */
+    function model()
+    {
+        return 'Longyi\Blog\Models\Comment';
+    }
+
+    /**
+     * Save blog tag.
+     *
+     * @param  array  $data
+     * @return bool|\Longyi\Blog\Contracts\Comment
+     */
+    public function save(array $data)
+    {
+        Event::dispatch('admin.blog.comments.create.before', $data);
+
+        $comment = $this->create($data);
+
+        Event::dispatch('admin.blog.comments.create.after', $comment);
+
+        return true;
+    }
+
+    /**
+     * Update item.
+     *
+     * @param  array  $data
+     * @param  int  $id
+     * @return bool
+     */
+    public function updateItem(array $data, $id)
+    {
+        Event::dispatch('admin.blog.comments.update.before', $id);
+
+        $tag = $this->update($data, $id);
+
+        Event::dispatch('admin.blog.comments.update.after', $tag);
+
+        return true;
+    }
+
+    /**
+     * Delete a blog tag item and delete the image from the disk or where ever it is.
+     *
+     * @param  int  $id
+     * @return bool
+     */
+    public function destroy($id)
+    {
+        Event::dispatch('admin.blog.comments.delete.before', $id);
+
+        parent::delete($id);
+
+        Event::dispatch('admin.blog.comments.delete.after', $id);
+    }
+
+    /**
+     * Get only active blog tags.
+     *
+     * @return array
+     */
+    public function getActiveBlogComments()
+    {
+        $currentLocale = core()->getCurrentLocale();
+
+        return $this->whereRaw("find_in_set(?, locale)", [$currentLocale->code])
+            ->orderBy('sort_order', 'ASC')
+            ->get()
+            ->toArray();
+    }
+}

+ 211 - 0
packages/Longyi/Blog/src/Repositories/BlogRepository.php

@@ -0,0 +1,211 @@
+<?php
+
+namespace Longyi\Blog\Repositories;
+
+use Carbon\Carbon;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Storage;
+use Longyi\Blog\Models\Category;
+use Webkul\Core\Eloquent\Repository;
+use Intervention\Image\ImageManager;
+use Illuminate\Support\Str;
+
+class BlogRepository extends Repository
+{
+    /**
+     * Specify Model class name
+     *
+     * @return mixed
+     */
+    function model()
+    {
+        return 'Longyi\Blog\Models\Blog';
+    }
+
+    /**
+     * Save blog.
+     *
+     * @param  array  $data
+     * @return bool|\Longyi\Blog\Contracts\Blog
+     */
+    public function save(array $data)
+    {
+        Event::dispatch('admin.blogs.create.before', $data);
+
+        $create_data = $data;
+
+        if ( array_key_exists('src', $create_data) ) {
+            unset($create_data['src']);
+        }
+
+        if ( array_key_exists('m_src', $create_data) ) {
+            unset($create_data['m_src']);
+        }
+
+        $blog = $this->create($create_data);
+
+        $this->uploadImages($data, $blog, 'src');
+
+        $this->uploadImages($data, $blog, 'm_src');
+
+        Event::dispatch('admin.blogs.create.after', $blog);
+
+        return true;
+    }
+
+    /**
+     * Update item.
+     *
+     * @param  array  $data
+     * @param  int  $id
+     * @return bool
+     */
+    public function updateItem(array $data, $id)
+    {
+        Event::dispatch('admin.blogs.update.before', $id);
+
+        $update_data = $data;
+
+        if ( array_key_exists('src', $update_data) ) {
+            unset($update_data['src']);
+        }
+
+        if ( array_key_exists('m_src', $update_data) ) {
+            unset($update_data['m_src']);
+        }
+
+        $blog = $this->update($update_data, $id);
+
+        $this->uploadImages($data, $blog, 'src');
+
+        $this->uploadImages($data, $blog, 'm_src');
+
+        Event::dispatch('admin.blogs.update.after', $blog);
+
+        return true;
+    }
+
+    /**
+     * Upload category's images.
+     *
+     * @param  array  $data
+     * @param  \Webkul\Category\Contracts\Category  $category
+     * @param  string  $type
+     * @return void
+     */
+    public function uploadImages($data, $blog, $type = 'src')
+    {
+        if (isset($data[$type])) {
+            foreach ($data[$type] as $imageId => $image) {
+                $file = $type . '.' . $imageId;
+
+                $dir = 'blog-images/' . $blog->id;
+
+                if (request()->hasFile($file)) {
+                    if ($blog->{$type}) {
+                        Storage::delete($blog->{$type});
+                    }
+
+                    $manager = new ImageManager();
+
+                    $image = $manager->make(request()->file($file))->encode('webp');
+
+                    $blog->{$type} = 'blog-images/' . $blog->id . '/' . Str::random(40) . '.webp';
+
+                    Storage::put($blog->{$type}, $image);
+
+                    $blog->save();
+                }
+            }
+        } else {
+            if ($blog->{$type}) {
+                Storage::delete($blog->{$type});
+            }
+
+            $blog->{$type} = null;
+
+            $blog->save();
+        }
+    }
+
+    /**
+     * Delete a blog item and delete the image from the disk or where ever it is.
+     *
+     * @param  int  $id
+     * @return bool
+     */
+    public function destroy($id)
+    {
+        $blogItem = $this->find($id);
+
+        $blogItemImage = $blogItem->src;
+
+        $blogItemMobileImage = $blogItem->m_src;
+
+        Storage::delete($blogItemImage);
+
+        Storage::delete($blogItemMobileImage);
+
+        return $this->model->destroy($id);
+    }
+
+    /**
+     * Get only active blogs.
+     *
+     * @return array
+     */
+    public function getActiveBlogs()
+    {
+        $locale = config('app.locale');
+
+        $blogs = DB::table('blogs')
+            ->where('published_at', '<=', Carbon::now()->format('Y-m-d'))
+            ->where('status', 1)
+            ->where('locale', $locale)
+            ->orderBy('id', 'DESC')
+            ->paginate(12);
+
+        return $blogs;
+    }
+
+    /**
+     * Get only single blogs.
+     *
+     * @return array
+     */
+    public function getSingleBlogs($id)
+    {
+        $blog = DB::table('blogs')
+            ->whereSlug($id)
+            ->where('published_at', '<=', Carbon::now()->format('Y-m-d'))
+            ->where('status', 1)
+            ->first();
+
+        return $blog;
+    }
+
+    /**
+     * Get only single blogs.
+     *
+     * @return array
+     */
+    public function getBlogCategories($id)
+    {
+        $locale = config('app.locale');
+
+        $categoryId = DB::table('blog_categories')
+            ->where('slug', $id)->first();
+
+        $blogs = DB::table('blogs')
+            ->where('published_at', '<=', Carbon::now()->format('Y-m-d'))
+            ->where('default_category', $categoryId['id'])
+            ->where('status', 1)
+            ->where('locale', $locale)
+            ->orderBy('id', 'DESC')
+            ->paginate(12);
+
+        return $blogs;
+    }
+}

+ 150 - 0
packages/Longyi/Blog/src/Repositories/BlogTagRepository.php

@@ -0,0 +1,150 @@
+<?php
+
+namespace Longyi\Blog\Repositories;
+
+use Webkul\Core\Eloquent\Repository;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Storage;
+use Intervention\Image\ImageManager;
+use Illuminate\Support\Str;
+
+class BlogTagRepository extends Repository
+{
+    /**
+     * Specify Model class name
+     *
+     * @return mixed
+     */
+    function model()
+    {
+        return 'Longyi\Blog\Models\Tag';
+    }
+
+    /**
+     * Save blog tag.
+     *
+     * @param  array  $data
+     * @return bool|\Longyi\Blog\Contracts\Tag
+     */
+    public function save(array $data)
+    {
+        Event::dispatch('admin.blog.tags.create.before', $data);
+
+        $create_data = $data;
+
+        if ( array_key_exists('image', $create_data) ) {
+            unset($create_data['image']);
+        }
+
+        $tags = $this->create($create_data);
+
+        $this->uploadImages($data, $tags);
+
+        Event::dispatch('admin.blog.tags.create.after', $tags);
+
+        return true;
+    }
+
+    /**
+     * Update item.
+     *
+     * @param  array  $data
+     * @param  int  $id
+     * @return bool
+     */
+    public function updateItem(array $data, $id)
+    {
+        Event::dispatch('admin.blog.tags.update.before', $id);
+
+        $update_data = $data;
+
+        if ( array_key_exists('image', $update_data) ) {
+            unset($update_data['image']);
+        }
+
+        $tag = $this->update($update_data, $id);
+
+        $this->uploadImages($data, $tag);
+
+        Event::dispatch('admin.blog.tags.update.after', $tag);
+
+        return true;
+    }
+
+    /**
+     * Upload tag's images.
+     *
+     * @param  array  $data
+     * @param  \Webkul\tag\Contracts\tag  $tag
+     * @param  string  $type
+     * @return void
+     */
+    public function uploadImages($data, $tag, $type = 'image')
+    {
+        if (isset($data[$type])) {
+            foreach ($data[$type] as $imageId => $image) {
+                $file = $type . '.' . $imageId;
+
+                $dir = 'blog-tag/' . $tag->id;
+
+                if (request()->hasFile($file)) {
+                    if ($tag->{$type}) {
+                        Storage::delete($tag->{$type});
+                    }
+
+                    $manager = new ImageManager();
+
+                    $image = $manager->make(request()->file($file))->encode('webp');
+
+                    $tag->{$type} = 'blog-tag/' . $tag->id . '/' . Str::random(40) . '.webp';
+
+                    Storage::put($tag->{$type}, $image);
+
+                    $tag->save();
+                }
+            }
+        } else {
+            if ($tag->{$type}) {
+                Storage::delete($tag->{$type});
+            }
+
+            $tag->{$type} = null;
+
+            $tag->save();
+        }
+    }
+
+    /**
+     * Delete a blog tag item and delete the image from the disk or where ever it is.
+     *
+     * @param  int  $id
+     * @return bool
+     */
+    public function destroy($id)
+    {
+       $tagItem = $this->find($id);
+
+        $tagItemImage = $tagItem->image;
+
+        Storage::delete($tagItemImage);
+
+        return $this->model->destroy($id);
+    }
+
+    /**
+     * Get only active blog tags.
+     *
+     * @return array
+     */
+    public function getActiveBlogTags()
+    {
+        $currentLocale = core()->getCurrentLocale();
+
+        return $this->whereRaw("find_in_set(?, locale)", [$currentLocale->code])
+            ->orderBy('sort_order', 'ASC')
+            ->get()
+            ->toArray();
+    }
+}

+ 221 - 0
packages/Longyi/Blog/src/Resources/assets/css/custom-blog.css

@@ -0,0 +1,221 @@
+.grid-wrap{
+                margin: 0 -10px;
+            }
+            .column-9{
+                width: 75%;
+                padding:0 10px;
+            }
+            .column-3{
+                width: 25%;
+                padding:0 10px;
+            }
+            .blog-grid-list{
+                margin: 0 -10px;
+            }
+            .blog-post-item{
+                width: 33.33%;
+                padding: 0 10px;
+                margin-bottom: 20px;
+            }
+            .blog-post-box{
+              border: 1px solid rgba(0,0,0,.125);  
+              padding: 15px;
+              border-radius: 10px;
+              height: 100%;
+            }
+            .blog-post-box .card-text{
+                overflow: hidden;
+                text-overflow: ellipsis;
+                display: -webkit-box;
+                -webkit-line-clamp: 3;
+                -webkit-box-orient: vertical;
+            }
+            .blog-post-box .post-meta{
+                font-size: 13px;
+                color: #8d8d8d;
+                margin-bottom: 6px;
+            }
+            .blog-grid-img{
+                overflow: hidden;
+                position: relative;
+                margin-bottom: 20px;
+            }
+            .blog-grid-img:before{
+                content: '';
+                padding-top: 65%;
+                display: block;
+            }
+            .blog-grid-img img{
+                position: absolute;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+            }
+            .blog-post-item .card-title{
+                font-size: 20px;
+                line-height: 1.2;
+                margin-bottom: 20px;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                display: -webkit-box;
+                -webkit-line-clamp: 2;
+                -webkit-box-orient: vertical;
+            }
+
+            .blog-post-item .card-footer{
+                margin-top: 20px;
+            }
+            .blog-post-item .card-footer a{
+                font-weight: 600;
+            }
+            .blog-post-item .card-title:hover,
+            .blog-post-item .card-footer a:hover{
+                color: #fbc256;
+            }
+            .blog-sidebar .categories{
+                padding-left: 20px;
+            }
+            .blog-sidebar .categories h3{
+                font-size: 20px;
+                line-height: 1.2;
+                margin-bottom: 15px;
+            }
+            .blog-sidebar .categories .list-group{
+                border: 1px solid rgba(0,0,0,.125);  
+                border-radius: 10px;
+            }
+            
+            .blog-sidebar .categories .list-group li{
+                border-top: 1px solid rgba(0,0,0,.125); 
+            }
+            .blog-sidebar .categories .list-group li:first-child{
+                border-top: 0;
+            }
+            .blog-sidebar .categories .list-group li a{
+                padding: 9px 15px;
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                font-size: 15px;
+            }
+            .blog-sidebar .categories .list-group li a .badge-primary{
+                background: #4d7ea8;
+                border-radius: 30px;
+                color: #fff;
+                font-size: 13px;
+                padding: 2px 10px;
+
+            }
+            .pagination{
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                gap: 10px;
+            }
+            .pagination a{
+                width: 30px;
+                height: 30px;
+                border: 1px solid #4d7ea8;
+                color: #000;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+            }
+            .pagination a:hover,
+            .pagination a.active{
+                background: #4d7ea8;
+                color: #fff;
+            }
+            .pagination a.previous:before{
+                content: '<<';
+                letter-spacing: -4px;
+            }
+            .pagination a.previous .icon,
+            .pagination a.next .icon{
+                display: none;
+            }
+            .pagination a.next:before{
+                content: '>>';
+                letter-spacing: -4px;
+            }
+            .page-title{
+                font-size: 22px;
+            }
+            .tags-part{
+                margin-top: 30px;
+            }
+            .tags-part .tag-list{
+                display: flex;
+                flex-wrap: wrap;
+                gap: 10px;
+            }
+            .tags-part .tag-list a{
+                background-color: #4d7ea8;
+                border-radius: 5px;
+                color: #fff;
+                font-size: 14px;
+                display: flex;
+                align-items: center;
+                padding: 3px 10px;
+                gap:5px;
+            }
+            .tags-part .tag-list a .badge-light{
+                background: #fff;
+                border-radius: 5px;
+                padding: 2px 6px;
+                font-size: 13px;
+                color: #000;
+                line-height: 1;
+            }
+
+            @media all and (max-width: 1180px){
+                .blog-sidebar .categories {
+                padding-left: 0px;
+            }
+
+            }
+
+            @media all and (max-width: 1024px){
+                .column-3,
+               .column-9 {
+                width: 100%;
+                padding: 0 10px;
+            }
+            .blog-post-item {
+                width: 50%;
+                padding: 0 10px;
+                margin-bottom: 20px;
+            }
+
+            }
+            @media all and (max-width: 767px){
+               .blog-hero-image .hero-main-title{
+                font-size: 1.5rem;
+               }
+               .blog-hero-image{
+                height: 270px;
+                margin-bottom: 30px;
+               }
+               .container {
+                    width: 100%;
+                    margin-right: auto;
+                    margin-left: auto;
+                    padding-right: 20px;
+                    padding-left: 20px;
+                }
+                .page-title {
+                    font-size: 22px;
+                    margin-bottom: 15px;
+                }
+                .comment-part .column-6 {
+                    width: 100%;
+                    padding: 0 10px;
+                }
+                .blog-post-item {
+                    width: 100%;
+                    padding: 0 10px;
+                    margin-bottom: 20px;
+                }
+            }

+ 12 - 0
packages/Longyi/Blog/src/Resources/assets/images/Icon-Temp-Active.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon-Catalog-Active</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Icon-Catalog-Active" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+        <g transform="translate(9.000000, 7.000000)" stroke="#0041FF" stroke-width="2">
+            <rect id="Rectangle-2" x="0" y="0" width="30" height="34"></rect>
+        </g>
+    </g>
+</svg>

+ 12 - 0
packages/Longyi/Blog/src/Resources/assets/images/Icon-Temp.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon-Catalog</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Icon-Catalog" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+        <g transform="translate(9.000000, 7.000000)" stroke-width="2">
+            <rect id="Rectangle-2" stroke="#8E8E8E" x="0" y="0" width="30" height="34"></rect>
+        </g>
+    </g>
+</svg>

+ 0 - 0
packages/Longyi/Blog/src/Resources/assets/js/app.js


+ 17 - 0
packages/Longyi/Blog/src/Resources/assets/sass/admin.scss

@@ -0,0 +1,17 @@
+.temp-icon {
+    width: 48px;
+    height: 48px;
+    display: inline-block;
+    background-size: cover;
+    background-image: url("../images/Icon-Temp.svg");
+}
+
+.active {
+    .temp-icon {
+        background-image: url("../images/Icon-Temp-Active.svg");
+    }
+
+    &.temp-icon {
+        background-image: url("../images/Icon-Temp-Active.svg");
+    }
+}

+ 0 - 0
packages/Longyi/Blog/src/Resources/assets/sass/default.scss


+ 0 - 0
packages/Longyi/Blog/src/Resources/assets/sass/velocity.scss


+ 130 - 0
packages/Longyi/Blog/src/Resources/lang/en/app.php

@@ -0,0 +1,130 @@
+<?php
+
+return [
+    'blog' => [
+        'title'                         => 'Posts',
+        'add-title'                     => 'Add Blog',
+        'edit-title'                    => 'Edit Blog',
+        'create-btn-title'              => 'Save Blog',
+        'edit-btn-title'                => 'Save Blog',
+        'general'                       => 'General',
+        'name'                          => 'Name',
+        'slug'                          => 'Slug',
+        'channels'                      => 'Channels',
+        'author'                        => 'Author',
+        'select_an_author'              => 'Select an author',
+        'published_at'                  => 'Published At',
+        'status'                        => 'Status',
+        'status-true'                   => 'Active',
+        'status-false'                  => 'Inactive',
+        'allow_comments'                => 'Allow Comments',
+        'yes'                           => 'Yes',
+        'no'                            => 'No',
+        'categories_tag'                => 'Categories & Tags',
+        'categories_title'              => 'Categories',
+        'tag_title'                     => 'Tags',
+        'default_category'              => 'Default Category',
+        'tags'                          => 'Tags',
+        'description-and-images'        => 'Description and Images',
+        'short_description'             => 'Short Description',
+        'description'                   => 'Description',
+        'image'                         => 'Image',
+        'add-image'                     => 'Add Image',
+        'mobile-image'                  => 'Mobile Image',
+        'add-mobile-image'              => 'Add Mobile Image',
+        'search_engine_optimization'    => 'Search Engine Optimization',
+        'meta_title'                    => 'Meta Title',
+        'meta_description'              => 'Meta Description',
+        'meta_keywords'                 => 'Meta Keywords',
+        'created-fault'                 => 'An error occurred while creating a blog.',
+        'updated-fault'                 => 'An error occurred while updating the blog.',
+        'delete-success'                => 'Blog deleted successfully',
+        'delete-failure'                => 'Blog cannot be deleted',
+    ],
+
+    'category' => [
+        'title'                         => 'Categories',
+        'add-title'                     => 'Add Blog Category',
+        'edit-title'                    => 'Edit Blog Category',
+        'create-btn-title'              => 'Save Category',
+        'edit-btn-title'                => 'Save Category',
+        'general'                       => 'General',
+        'name'                          => 'Name',
+        'slug'                          => 'Slug',
+        'status'                        => 'Stauts',
+        'status-true'                   => 'Active',
+        'status-false'                  => 'Inactive',
+        'description'                   => 'Description',
+        'image'                         => 'Image',
+        'add-image'                     => 'Add Image',
+        'search_engine_optimization'    => 'Search Engine Optimization',
+        'meta_title'                    => 'Meta Title',
+        'meta_description'              => 'Meta Description',
+        'meta_keywords'                 => 'Meta Keywords',
+        'created-fault'                 => 'An error occurred while creating a category.',
+        'updated-fault'                 => 'An error occurred while updating the category.',
+        'delete-success'                => 'Category deleted successfully',
+        'delete-failure'                => 'Category cannot be deleted',
+    ],
+
+    'tag' => [
+        'title'                         => 'Tags',
+        'add-title'                     => 'Add Blog Tag',
+        'edit-title'                    => 'Edit Blog Tag',
+        'create-btn-title'              => 'Save Tag',
+        'edit-btn-title'                => 'Save Tag',
+        'general'                       => 'General',
+        'name'                          => 'Name',
+        'slug'                          => 'Slug',
+        'status'                        => 'Stauts',
+        'status-true'                   => 'Active',
+        'status-false'                  => 'Inactive',
+        'description'                   => 'Description',
+        'image'                         => 'Image',
+        'add-image'                     => 'Add Image',
+        'search_engine_optimization'    => 'Search Engine Optimization',
+        'meta_title'                    => 'Meta Title',
+        'meta_description'              => 'Meta Description',
+        'meta_keywords'                 => 'Meta Keywords',
+        'created-fault'                 => 'An error occurred while creating a tag.',
+        'updated-fault'                 => 'An error occurred while updating the tag.',
+        'delete-success'                => 'Tag deleted successfully',
+        'delete-failure'                => 'Tag cannot be deleted',
+    ],
+
+    'comment' => [
+        'title'                         => 'Comments',
+        'edit-title'                    => 'Edit Comment',
+        'edit-btn-title'                => 'Save Comment',
+        'general'                       => 'General',
+        'post'                          => 'Post',
+        'name'                          => 'Name',
+        'email'                         => 'Email',
+        'comment_date'                  => 'Comment Date',
+        'status'                        => 'Status',
+        'status-pending'                => 'Pending',
+        'status-approved'               => 'Approved',
+        'status-rejected'               => 'Rejected',
+        'comment'                       => 'Comment',
+        'delete-success'                => 'Comment deleted successfully',
+        'delete-failure'                => 'Comment cannot be deleted',
+    ],
+
+    'datagrid' => [
+        'id'                            => 'ID',
+        'name'                          => 'Name',
+        'content'                       => 'Content',
+        'author'                        => 'Author',
+        'category'                      => 'Categories',
+        'status'                        => 'Status',
+        'allow_comments'                => 'Allow Comments',
+        'published_at'                  => 'Published At',
+        'actions'                       => 'Actions',
+        'pending'                       => 'Pending',
+        'approved'                      => 'Approved',
+        'rejected'                      => 'Rejected',
+        'edit'                          => 'Edit',
+        'delete'                        => 'Delete',
+        'view'                          => 'View',
+    ],
+];

+ 125 - 0
packages/Longyi/Blog/src/Resources/lang/pt_BR/app.php

@@ -0,0 +1,125 @@
+<?php
+
+return [
+    'blog' => [
+        'title'                         => 'Postagens',
+        'add-title'                     => 'Adicionar Blog',
+        'edit-title'                    => 'Editar Blog',
+        'create-btn-title'              => 'Salvar Blog',
+        'edit-btn-title'                => 'Salvar Blog',
+        'general'                       => 'Em Geral',
+        'name'                          => 'Primeiro Nome',
+        'slug'                          => 'Slug',
+        'channels'                      => 'Canais',
+        'author'                        => 'Escritor',
+        'select_an_author'              => 'Escolha um autor',
+        'published_at'                  => 'Data de Lançamento',
+        'status'                        => 'Situação',
+        'status-true'                   => 'Ativo',
+        'status-false'                  => 'Passiva',
+        'allow_comments'                => 'Permitir comentários',
+        'yes'                           => 'Sim',
+        'no'                            => 'Não',
+        'categories_tag'                => 'Categorias & Etiquetas',
+        'default_category'              => 'Categoria Padrão',
+        'tags'                          => 'Etiquetas',
+        'short_description'             => 'Pequena Descrição',
+        'description'                   => 'Descrição',
+        'image'                         => 'Imagem',
+        'add-image'                     => 'Adicionar Imagem',
+        'search_engine_optimization'    => 'Motor de Otimização de Busca',
+        'meta_title'                    => 'Meta Título',
+        'meta_description'              => 'Meta Descrição',
+        'meta_keywords'                 => 'Palavras Chave',
+        'created-fault'                 => 'Ocorreu um erro ao criar o blog.',
+        'updated-fault'                 => 'Ocorreu um erro ao atualizar o blog.',
+        'delete-success'                => 'O blog foi excluído com sucesso.',
+        'delete-failure'                => 'Erro ao excluir blog!',
+    ],
+
+    'category' => [
+        'title'                         => 'Categorias',
+        'add-title'                     => 'Adicionar Categoria',
+        'edit-title'                    => 'Editar Categoria',
+        'create-btn-title'              => 'Salvar Categoria',
+        'edit-btn-title'                => 'Salvar Categoria',
+        'general'                       => 'Em Geral',
+        'name'                          => 'Primeiro Nome',
+        'slug'                          => 'Slug',
+        'status'                        => 'Situação',
+        'status-true'                   => 'Ativo',
+        'status-false'                  => 'Passiva',
+        'description'                   => 'Descrição',
+        'image'                         => 'Imagem',
+        'add-image'                     => 'Adicionar Imagem',
+        'search_engine_optimization'    => 'Motor de Otimização de Busca',
+        'meta_title'                    => 'Meta Título',
+        'meta_description'              => 'Meta Descrição',
+        'meta_keywords'                 => 'Palavras Chave',
+        'created-fault'                 => 'Ocorreu um erro ao criar a categoria.',
+        'updated-fault'                 => 'Ocorreu um erro ao atualizar a categoria.',
+        'delete-success'                => 'A categoria foi excluída com sucesso.',
+        'delete-failure'                => 'Erro ao excluir categoria!',
+    ],
+
+    'tag' => [
+        'title'                         => 'Etiquetas',
+        'add-title'                     => 'Adicionar Etiquetas',
+        'edit-title'                    => 'Editar Etiquetas',
+        'create-btn-title'              => 'Salvar Etiquetas',
+        'edit-btn-title'                => 'Salvar Etiquetas',
+        'general'                       => 'Em Geral',
+        'name'                          => 'Primeiro Nome',
+        'slug'                          => 'Slug',
+        'status'                        => 'Situação',
+        'status-true'                   => 'Ativo',
+        'status-false'                  => 'Passiva',
+        'description'                   => 'Descrição',
+        'image'                         => 'Imagem',
+        'add-image'                     => 'AdicionarImagem',
+        'search_engine_optimization'    => 'Motor de Otimização de Busca',
+        'meta_title'                    => 'Meta Título',
+        'meta_description'              => 'Meta Descrição',
+        'meta_keywords'                 => 'Palavras Chave',
+        'created-fault'                 => 'Ocorreu um erro ao criar a tag.',
+        'updated-fault'                 => 'Ocorreu um erro ao atualizar a tag.',
+        'delete-success'                => 'A etiqueta foi eliminada com sucesso.',
+        'delete-failure'                => 'Erro ao excluir a etiqueta!',
+    ],
+
+    'comment' => [
+        'title'                         => 'Comentários',
+        'edit-title'                    => 'Editar Comentário',
+        'edit-btn-title'                => 'Salvar comentário',
+        'general'                       => 'Em Geral',
+        'post'                          => 'Publicar',
+        'name'                          => 'Primeiro Nome',
+        'email'                         => 'Email',
+        'comment_date'                  => 'Data do Comentário',
+        'status'                        => 'Situação',
+        'status-pending'                => 'Esperando',
+        'status-approved'               => 'Aprovado',
+        'status-rejected'               => 'Negado',
+        'comment'                       => 'Comente',
+        'delete-success'                => 'Comentário excluído com sucesso.',
+        'delete-failure'                => 'Erro ao excluir o comentário!',
+    ],
+
+    'datagrid' => [
+        'id'                            => 'ID',
+        'name'                          => 'Primeiro Nome',
+        'content'                       => 'Conteúdo',
+        'author'                        => 'Escritor',
+        'category'                      => 'Categoria',
+        'status'                        => 'Situação',
+        'allow_comments'                => 'Permitir Comentários',
+        'published_at'                  => 'Data de Lançamento',
+        'actions'                       => 'Açao',
+        'pending'                       => 'Esperando',
+        'approved'                      => 'Aprovado',
+        'rejected'                      => 'Negado',
+        'edit'                          => 'Editar',
+        'delete'                        => 'Excluir',
+        'view'                          => 'Visualizar',
+    ],
+];

+ 125 - 0
packages/Longyi/Blog/src/Resources/lang/tr/app.php

@@ -0,0 +1,125 @@
+<?php
+
+return [
+    'blog' => [
+        'title'                         => 'Gönderiler',
+        'add-title'                     => 'Blog Ekle',
+        'edit-title'                    => 'Blog Düzenle',
+        'create-btn-title'              => 'Blog Kaydet',
+        'edit-btn-title'                => 'Blog Kaydet',
+        'general'                       => 'Genel',
+        'name'                          => 'Adı',
+        'slug'                          => 'Slug',
+        'channels'                      => 'Kanallar',
+        'author'                        => 'Yazar',
+        'select_an_author'              => 'Bir yazar seçin',
+        'published_at'                  => 'Yayınlanma Tarihi',
+        'status'                        => 'Durum',
+        'status-true'                   => 'Aktif',
+        'status-false'                  => 'Pasif',
+        'allow_comments'                => 'Yorumlara izin ver',
+        'yes'                           => 'Evet',
+        'no'                            => 'Hayır',
+        'categories_tag'                => 'Kategoriler & Etiketler',
+        'default_category'              => 'Varsayılan Kategori',
+        'tags'                          => 'Etiketler',
+        'short_description'             => 'Kısa Açıklama',
+        'description'                   => 'Açıklama',
+        'image'                         => 'Görüntü',
+        'add-image'                     => 'Görüntü Ekle',
+        'search_engine_optimization'    => 'Arama Motoru Optimizasyonu',
+        'meta_title'                    => 'Meta Başlık',
+        'meta_description'              => 'Meta Açıklama',
+        'meta_keywords'                 => 'Anahtar Kelimeler',
+        'created-fault'                 => 'Blog oluşturulurken hata meydana geldi.',
+        'updated-fault'                 => 'Kategori güncellenirken hata meydana geldi.',
+        'delete-success'                => 'Blog başarıyla silindi.',
+        'delete-failure'                => 'Blog silinirken hata oluştu!',
+    ],
+
+    'category' => [
+        'title'                         => 'Kategoriler',
+        'add-title'                     => 'Kategori Ekle',
+        'edit-title'                    => 'Kategori Düzenle',
+        'create-btn-title'              => 'Kategori Kaydet',
+        'edit-btn-title'                => 'Kategori Kaydet',
+        'general'                       => 'Genel',
+        'name'                          => 'Adı',
+        'slug'                          => 'Slug',
+        'status'                        => 'Durum',
+        'status-true'                   => 'Aktif',
+        'status-false'                  => 'Pasif',
+        'description'                   => 'Açıklama',
+        'image'                         => 'Görüntü',
+        'add-image'                     => 'Görüntü Ekle',
+        'search_engine_optimization'    => 'Arama Motoru Optimizasyonu',
+        'meta_title'                    => 'Meta Başlık',
+        'meta_description'              => 'Meta Açıklama',
+        'meta_keywords'                 => 'Anahtar Kelimeler',
+        'created-fault'                 => 'Kategori oluşturulurken hata meydana geldi.',
+        'updated-fault'                 => 'Kategori güncellenirken hata meydana geldi.',
+        'delete-success'                => 'Kategori başarıyla silindi.',
+        'delete-failure'                => 'Kategori silinirken hata oluştu!',
+    ],
+
+    'tag' => [
+        'title'                         => 'Etiketler',
+        'add-title'                     => 'Etiket Ekle',
+        'edit-title'                    => 'Etiket Düzenle',
+        'create-btn-title'              => 'Etiket Kaydet',
+        'edit-btn-title'                => 'Etiket Kaydet',
+        'general'                       => 'Genel',
+        'name'                          => 'Adı',
+        'slug'                          => 'Slug',
+        'status'                        => 'Durum',
+        'status-true'                   => 'Aktif',
+        'status-false'                  => 'Pasif',
+        'description'                   => 'Açıklama',
+        'image'                         => 'Görüntü',
+        'add-image'                     => 'Görüntü Ekle',
+        'search_engine_optimization'    => 'Arama Motoru Optimizasyonu',
+        'meta_title'                    => 'Meta Başlık',
+        'meta_description'              => 'Meta Açıklama',
+        'meta_keywords'                 => 'Anahtar Kelimeler',
+        'created-fault'                 => 'Etiket oluşturulurken hata meydana geldi.',
+        'updated-fault'                 => 'Etiket güncellenirken hata meydana geldi.',
+        'delete-success'                => 'Etiket başarıyla silindi.',
+        'delete-failure'                => 'Etiket silinirken hata oluştu!',
+    ],
+
+    'comment' => [
+        'title'                         => 'Yorumlar',
+        'edit-title'                    => 'Yorumu Düzenle',
+        'edit-btn-title'                => 'Yorum Kaydet',
+        'general'                       => 'Genel',
+        'post'                          => 'Gönderi',
+        'name'                          => 'Adı',
+        'email'                         => 'Email',
+        'comment_date'                  => 'Yorum Tarihi',
+        'status'                        => 'Durum',
+        'status-pending'                => 'Bekliyor',
+        'status-approved'               => 'Onaylı',
+        'status-rejected'               => 'Reddedildi',
+        'comment'                       => 'Yorum',
+        'delete-success'                => 'Yorum başarıyla silindi.',
+        'delete-failure'                => 'Yorum silinirken hata oluştu!',
+    ],
+
+    'datagrid' => [
+        'id'                            => 'ID',
+        'name'                          => 'Adı',
+        'content'                       => 'İçerik',
+        'author'                        => 'Yazar',
+        'category'                      => 'Kategori',
+        'status'                        => 'Durum',
+        'allow_comments'                => 'Yorumlara İzin Ver',
+        'published_at'                  => 'Yayınlanma Tarihi',
+        'actions'                       => 'Aksiyon',
+        'pending'                       => 'Bekliyor',
+        'approved'                      => 'Onaylandı',
+        'rejected'                      => 'Reddedildi',
+        'edit'                          => 'Düzenle',
+        'delete'                        => 'Sil',
+        'view'                          => 'Görüntüle',
+    ],
+];

+ 646 - 0
packages/Longyi/Blog/src/Resources/views/admin/blogs/create.blade.php

@@ -0,0 +1,646 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('blog::app.blog.add-title')
+    </x-slot:title>
+
+    @pushOnce('styles')
+
+        <style type="text/css">
+            
+            .v-tree-container>.v-tree-item:not(.has-children) {
+                padding-left: 18px !important;
+            }
+            
+        </style>
+
+    @endPushOnce
+
+    @php
+        $currentLocale = core()->getRequestedLocale();
+    @endphp
+    
+    <!-- Blog Edit Form -->
+    <x-admin::form
+        :action="route('admin.blog.store')"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        {!! view_render_event('admin.blogs.create.before') !!}
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                @lang('blog::app.blog.add-title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <!-- Cancel Button -->
+                <a
+                    href="{{ route('admin.blog.index') }}"
+                    class="transparent-button hover:bg-gray-200 dark:hover:bg-gray-800 dark:text-white "
+                >
+                    @lang('admin::app.catalog.categories.create.back-btn')
+                </a>
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                >
+                    @lang('blog::app.blog.create-btn-title')
+                </button>
+            </div>
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <!-- Left Section -->
+            <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+
+                <!-- General -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('admin::app.catalog.categories.create.general')
+                    </p>
+
+                    <!-- Locales -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="locale"
+                        value="{{ core()->getRequestedLocaleCode() }}"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Channel -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="channels"
+                        value="1"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Name -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.blog.name')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="name"
+                            value="{{ old('name') }}"
+                            label="{{ trans('blog::app.blog.name') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="name"
+                                id="name"
+                                v-bind="field"
+                                :class="[errors['{{ 'name' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.blog.name') }}"
+                                v-slugify-target:slug="setValues"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="name"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Slug -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('admin::app.catalog.categories.create.slug')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="slug"
+                            value="{{ old('slug') }}"
+                            label="{{ trans('admin::app.catalog.categories.create.slug') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="slug"
+                                id="slug"
+                                v-bind="field"
+                                :class="[errors['{{ 'slug' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('admin::app.catalog.categories.create.slug') }}"
+                                v-slugify-target:slug
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="slug"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                </div>
+
+                <!-- Description and images -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.blog.description-and-images')
+                    </p>
+
+                    <!-- Meta Description -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.blog.short_description')
+                        </x-admin::form.control-group.label>
+
+                        <x-admin::form.control-group.control
+                            type="textarea"
+                            name="short_description"
+                            id="short_description"
+                            rules="required"
+                            :value="old('short_description')"
+                            :label="trans('blog::app.blog.short_description')"
+                            :placeholder="trans('blog::app.blog.short_description')"
+                        >
+                        </x-admin::form.control-group.control>
+
+                        <x-admin::form.control-group.error control-name="short_description"></x-admin::form.control-group.error>
+
+                    </x-admin::form.control-group>
+
+                    <!-- Description -->
+                    <v-description>
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="description"
+                                id="description"
+                                class="description"
+                                rules="required"
+                                :value="old('description')"
+                                :label="trans('blog::app.blog.description')"
+                                :tinymce="true"
+                                :prompt="core()->getConfigData('general.magic_ai.content_generation.category_description_prompt')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="description"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+                    </v-description>
+
+                    <div class="flex gap-12">
+                        <!-- Add Logo -->
+                        <div class="flex flex-col gap-2 w-2/5 mt-5">
+                            <p class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.image')
+                            </p>
+
+                            <x-admin::media.images name="src"></x-admin::media.images>
+
+                            <x-admin::form.control-group.error control-name="src"></x-admin::form.control-group.error>
+
+                        </div>
+
+                        <!-- Add Mobile Logo -->
+                        <div class="flex flex-col gap-2 w-2/5 mt-5">
+                            <p class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.mobile-image')
+                            </p>
+
+                            <x-admin::media.images name="m_src"></x-admin::media.images>
+
+                            <x-admin::form.control-group.error control-name="m_src"></x-admin::form.control-group.error>
+
+                        </div>
+
+                    </div>
+                </div>
+
+                <!-- SEO Deatils -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.blog.search_engine_optimization')
+                    </p>
+
+                    <!-- SEO Title & Description Blade Componnet -->
+                    {{-- <x-admin::seo/> --}}
+                    <v-seo-helper-custom></v-seo-helper-custom>
+
+                    <div class="mt-8">
+                        <!-- Meta Title -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_title')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_title"
+                                id="meta_title"
+                                rules="required"
+                                :value="old('meta_title')"
+                                :label="trans('blog::app.blog.meta_title')"
+                                :placeholder="trans('blog::app.blog.meta_title')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_title"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Keywords -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_keywords')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_keywords"
+                                rules="required"
+                                :value="old('meta_keywords')"
+                                :label="trans('blog::app.blog.meta_keywords')"
+                                :placeholder="trans('blog::app.blog.meta_keywords')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_keywords"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Description -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="meta_description"
+                                id="meta_description"
+                                rules="required"
+                                :value="old('meta_description')"
+                                :label="trans('blog::app.blog.meta_description')"
+                                :placeholder="trans('blog::app.blog.meta_description')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_description"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+                    </div>
+                </div>
+
+            </div>
+
+            <!-- Right Section -->
+            <div class="flex flex-col gap-2 w-[360px] max-w-full">
+                <!-- Settings -->
+
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-800 dark:text-gray-300 text-base font-semibold">
+                            @lang('admin::app.catalog.categories.create.settings')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Published At -->
+                        <x-admin::form.control-group class="w-full mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.published_at')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="date"
+                                name="published_at" 
+                                id="published_at"
+                                rules="required"
+                                :value="old('published_at') ?? date('Y-m-d')"
+                                :label="trans('blog::app.blog.published_at')"
+                                :placeholder="trans('blog::app.blog.published_at')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="published_at"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+
+                        <!-- Status -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.status')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="status"
+                                class="cursor-pointer"
+                                value="1"
+                                :label="trans('blog::app.blog.status')"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        <!-- Allow Comments -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.allow_comments')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="allow_comments"
+                                class="cursor-pointer"
+                                value="1"
+                                :label="trans('blog::app.blog.allow_comments')"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        <!-- Auther -->
+                        @php
+
+                        $loggedIn_user = auth()->guard('admin')->user()->toarray();
+                        $user_id = ( array_key_exists('id', $loggedIn_user) ) ? $loggedIn_user['id'] : 0;
+                        $user_name = ( array_key_exists('name', $loggedIn_user) ) ? $loggedIn_user['name'] : '';
+                        $role = ( array_key_exists('role', $loggedIn_user) ) ? ( array_key_exists('name', $loggedIn_user['role']) ? $loggedIn_user['role']['name'] : 'Administrator' ) : 'Administrator';
+
+                        @endphp
+
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required text-gray-800 dark:text-white font-medium required">
+                                @lang('blog::app.blog.author')
+                            </x-admin::form.control-group.label>
+
+                            @if( $role != 'Administrator' )
+                                <input type="hidden" name="author_id" id="author_id" value="{{$user_id}}">
+                                <x-admin::form.control-group.control
+                                    type="text"
+                                    name="author"
+                                    rules="required"
+                                    disabled="disabled"
+                                    :value="$user_name"
+                                    :label="trans('blog::app.blog.author')"
+                                    :placeholder="trans('blog::app.blog.author')"
+                                >
+                                </x-admin::form.control-group.control>
+                            @else
+                                <x-admin::form.control-group.control
+                                    type="select"
+                                    name="author_id"
+                                    id="author_id"
+                                    {{-- class="cursor-pointer" --}}
+                                    rules="required"
+                                    :value="old('author_id') ?? $user_id"
+                                    :label="trans('blog::app.blog.author')"
+                                    {{-- :placeholder="trans('blog::app.blog.author')" --}}
+                                >
+                                    <!-- Options -->
+                                    <option value="">Select an author</option>
+                                    @foreach($users as $user)
+                                        <option value="{{$user->id}}">{{$user->name}}</option>
+                                    @endforeach
+                                </x-admin::form.control-group.control>
+
+                                <x-admin::form.control-group.error
+                                    control-name="author"
+                                >
+                                </x-admin::form.control-group.error>
+                            @endif
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                <!-- Default Categories -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="required p-2.5 text-gray-800 dark:text-gray-300 text-base font-semibold">
+                            {{-- @lang('blog::app.blog.categories_title') --}}
+                            Default Category
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Category -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            {{-- <x-admin::form.control-group.label class="required text-gray-800 dark:text-white font-medium required">
+                                @lang('blog::app.blog.default_category')
+                            </x-admin::form.control-group.label> --}}
+
+                            <x-admin::form.control-group.control
+                                type="select"
+                                name="default_category"
+                                id="default_category"
+                                {{-- class="cursor-pointer" --}}
+                                rules="required"
+                                :value="old('default_category')"
+                                :label="trans('blog::app.blog.default_category')"
+                            >
+                                <!-- Options -->
+                                <option value="">Select an category</option>
+                                @foreach($categories as $category)
+                                    <option value="{{$category->id}}" data-slug="{{$category->slug}}" id="{{'default_category'.$category->id}}" >{{$category->name}}</option>
+                                @endforeach
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="default_category"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                <!-- Additional Categories -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-800 dark:text-gray-300 text-base font-semibold">
+                            {{-- @lang('blog::app.blog.categories_title') --}}
+                            Additional Category
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <div class="flex flex-col gap-[12px]">
+                            <x-admin::tree.view
+                                input-type="checkbox"
+                                name-field="categorys"
+                                id-field="id"
+                                value-field="id"
+                                :items="json_encode($additional_categories)"
+                                behavior="no"
+                                :fallback-locale="config('app.fallback_locale')"
+                            >
+                            </x-admin::tree.view>
+                        </div>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                <!-- Tags -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="required p-2.5 text-gray-800 dark:text-gray-300 text-base font-semibold">
+                            @lang('blog::app.blog.tag_title')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+                        @foreach ($tags as $tag)
+                            <x-admin::form.control-group class="flex gap-2.5 !mb-0 p-1.5">
+                                <x-admin::form.control-group.control
+                                    type="checkbox"
+                                    name="tags[]"
+                                    :id="$tag->name"
+                                    :value="$tag->id"
+                                    rules="required"
+                                    :for="$tag->name"
+                                    :label="trans('blog::app.blog.tags')"
+                                >
+                                </x-admin::form.control-group.control>
+
+                                <x-admin::form.control-group.label
+                                    :for="$tag->name"
+                                    class="!text-sm !text-gray-600 dark:!text-gray-300 font-semibold cursor-pointer"
+                                >
+                                    {{ $tag->name }}
+                                </x-admin::form.control-group.label>
+                            </x-admin::form.control-group>
+                        @endforeach
+
+                        <x-admin::form.control-group.error
+                            control-name="tags[]"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                {!! view_render_event('admin.blogs.create.after') !!}
+
+            </div>
+        </div>
+
+    </x-admin::form>
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-seo-helper-custom-template">
+        <div class="flex flex-col gap-[3px] mb-[30px]">
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                v-text="metaTitle"
+            >
+            </p>
+
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                style="display: none;"
+                v-text="metaSlug"
+            >
+            </p>
+
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                style="display: none;"
+                v-text="metaSlugCategory"
+            >
+            </p>
+
+            <!-- SEO Meta Title -->
+            <p 
+                class="text-[#135F29]"
+                v-text="'{{ URL::to('/') }}/blog' + ( ( metaSlugCategory != '' && metaSlugCategory != null && metaSlugCategory != undefined ) ? '/'+metaSlugCategory : '' ) + '/' + (metaSlug ? metaSlug.toLowerCase().replace(/\s+/g, '-') : '')"
+            >
+            </p>
+
+            <!-- SEP Meta Description -->
+            <p 
+                class="text-gray-600 dark:text-gray-300"
+                v-text="metaDescription"
+            >
+            </p>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-seo-helper-custom', {
+            template: '#v-seo-helper-custom-template',
+
+            data() {
+                return {
+                    metaTitle: this.$parent.getValues()['meta_title'],
+
+                    metaDescription: this.$parent.getValues()['meta_description'],
+
+                    metaSlug: this.$parent.getValues()['slug'],
+
+                    metaSlugCategory: this.$parent.getValues()['default_category'],
+                }
+            },
+
+            mounted() {
+                let self = this;
+
+                self.metaTitle = document.getElementById('meta_title').value;
+
+                self.metaDescription = document.getElementById('meta_description').value;
+
+                self.metaSlug = document.getElementById('slug').value;
+
+                var d_cat_id = document.getElementById('default_category').value;
+
+                var d_cat_slug = document.getElementById('default_category' + d_cat_id).getAttribute("data-slug");
+
+                self.metaSlugCategory = ( d_cat_slug != '' && d_cat_slug != null && d_cat_slug != undefined ) ? d_cat_slug : '';
+
+                document.getElementById('meta_title').addEventListener('input', function(e) {
+                    self.metaTitle = e.target.value;
+                });
+
+                document.getElementById('meta_description').addEventListener('input', function(e) {
+                    self.metaDescription = e.target.value;
+                });
+
+                document.getElementById('name').addEventListener('input', function(e) {
+                    setTimeout(function(){
+                        var slug = document.getElementById('slug').value;
+                        self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                    }, 1000);
+                });
+
+                document.getElementById('slug').addEventListener('input', function(e) {
+                    var slug = e.target.value;
+                    self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                });
+
+                document.getElementById('default_category').addEventListener('change', function(e) {
+                    var cat_slug = document.getElementById('default_category' + e.target.value).getAttribute("data-slug");
+                    self.metaSlugCategory = ( cat_slug != '' && cat_slug != null && cat_slug != undefined ) ? cat_slug : '';
+                });
+            },
+        });
+    </script>
+@endPushOnce
+
+</x-admin::layouts>

+ 670 - 0
packages/Longyi/Blog/src/Resources/views/admin/blogs/edit.blade.php

@@ -0,0 +1,670 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('blog::app.blog.edit-title')
+    </x-slot:title>
+
+    @pushOnce('styles')
+
+        <style type="text/css">
+            
+            .v-tree-container>.v-tree-item:not(.has-children) {
+                padding-left: 18px !important;
+            }
+            
+        </style>
+
+    @endPushOnce
+
+    @php
+        $currentLocale = core()->getRequestedLocale();
+    @endphp
+    
+    <!-- Blog Edit Form -->
+    <x-admin::form
+        :action="route('admin.blog.update', $blog->id)"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        {!! view_render_event('admin.blogs.edit.before') !!}
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                @lang('blog::app.blog.edit-title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <!-- Back Button -->
+                <a
+                    href="{{ route('admin.blog.index') }}"
+                    class="transparent-button hover:bg-gray-200 dark:hover:bg-gray-800 dark:text-white"
+                >
+                    @lang('admin::app.catalog.categories.edit.back-btn')
+                </a>
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                >
+                    @lang('blog::app.blog.edit-btn-title')
+                </button>
+            </div>
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <!-- Left Section -->
+            <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+
+                <!-- General -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('admin::app.catalog.categories.create.general')
+                    </p>
+
+                    <!-- Locales -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="locale"
+                        value="{{ core()->getRequestedLocaleCode() }}"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Channel -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="channels"
+                        value="1"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Name -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.blog.name')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="name"
+                            value="{{ old('name') ?? $blog->name }}"
+                            label="{{ trans('blog::app.blog.name') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="name"
+                                id="name"
+                                v-bind="field"
+                                :class="[errors['{{ 'name' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.blog.name') }}"
+                                v-slugify-target:slug="setValues"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="name"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Slug -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('admin::app.catalog.categories.create.slug')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="slug"
+                            value="{{ old('slug') ?? $blog->slug }}"
+                            label="{{ trans('admin::app.catalog.categories.create.slug') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="slug"
+                                id="slug"
+                                v-bind="field"
+                                :class="[errors['{{ 'slug' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('admin::app.catalog.categories.create.slug') }}"
+                                v-slugify-target:slug
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="slug"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                </div>
+
+                <!-- Description and images -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.blog.description-and-images')
+                    </p>
+
+                    <!-- Meta Description -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.blog.short_description')
+                        </x-admin::form.control-group.label>
+
+                        <x-admin::form.control-group.control
+                            type="textarea"
+                            name="short_description"
+                            id="short_description"
+                            rules="required"
+                            :value="old('short_description') ?? $blog->short_description"
+                            :label="trans('blog::app.blog.short_description')"
+                            :placeholder="trans('blog::app.blog.short_description')"
+                        >
+                        </x-admin::form.control-group.control>
+
+                        <x-admin::form.control-group.error control-name="short_description"></x-admin::form.control-group.error>
+
+                    </x-admin::form.control-group>
+
+                    <!-- Description -->
+                    <v-description>
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="description"
+                                id="description"
+                                class="description"
+                                rules="required"
+                                :value="old('description') ?? $blog->description"
+                                :label="trans('blog::app.blog.description')"
+                                :tinymce="true"
+                                :prompt="core()->getConfigData('general.magic_ai.content_generation.category_description_prompt')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="description"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+                    </v-description>
+
+                    <div class="flex gap-12">
+                        <!-- Add Logo -->
+                        <div class="flex flex-col gap-2 w-2/5 mt-5">
+                            <p class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.image')
+                            </p>
+
+                            <x-admin::media.images 
+                                name="src" 
+                                :uploaded-images="$blog->src ? [['id' => 'src', 'url' => $blog->src_url]] : []"
+                            >
+                                
+                            </x-admin::media.images>
+
+                        </div>
+
+                        <!-- Add Mobile Logo -->
+                        <div class="flex flex-col gap-2 w-2/5 mt-5">
+                            <p class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.mobile-image')
+                            </p>
+
+                            <x-admin::media.images 
+                                name="m_src" 
+                                :uploaded-images="$blog->m_src ? [['id' => 'm_src', 'url' => $blog->m_src_url]] : []"
+                            >
+                                
+                            </x-admin::media.images>
+
+                        </div>
+
+                    </div>
+                </div>
+
+                <!-- SEO Deatils -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="text-base text-gray-800 dark:text-white font-semibold mb-4">
+                        @lang('blog::app.blog.search_engine_optimization')
+                    </p>
+
+                    <!-- SEO Title & Description Blade Componnet -->
+                    {{-- <x-admin::seo/> --}}
+                    <v-seo-helper-custom></v-seo-helper-custom>
+
+                    <div class="mt-8">
+                        <!-- Meta Title -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_title')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_title"
+                                id="meta_title"
+                                rules="required"
+                                :value="old('meta_title') ?? $blog->meta_title"
+                                :label="trans('blog::app.blog.meta_title')"
+                                :placeholder="trans('blog::app.blog.meta_title')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_title"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Keywords -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_keywords')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_keywords"
+                                rules="required"
+                                :value="old('meta_keywords') ?? $blog->meta_keywords"
+                                :label="trans('blog::app.blog.meta_keywords')"
+                                :placeholder="trans('blog::app.blog.meta_keywords')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_keywords"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Description -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="meta_description"
+                                id="meta_description"
+                                rules="required"
+                                :value="old('meta_description') ?? $blog->meta_description"
+                                :label="trans('blog::app.blog.meta_description')"
+                                :placeholder="trans('blog::app.blog.meta_description')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_description"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+                    </div>
+                </div>
+
+            </div>
+
+            <!-- Right Section -->
+            <div class="flex flex-col gap-2 w-[360px] max-w-full">
+                <!-- Settings -->
+
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-800 dark:text-gray-300 text-base font-semibold">
+                            @lang('admin::app.catalog.categories.create.settings')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Published At -->
+                        <x-admin::form.control-group class="w-full mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.published_at')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="date"
+                                name="published_at" 
+                                id="published_at"
+                                rules="required"
+                                :value="old('published_at') ?? date_format(date_create($blog->published_at),'Y-m-d')"
+                                :label="trans('blog::app.blog.published_at')"
+                                :placeholder="trans('blog::app.blog.published_at')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="published_at"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+
+                        <!-- Status -->
+                        <input type="hidden" name="status" id="status" value="@php echo $blog->status @endphp">
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.status')
+                            </x-admin::form.control-group.label>
+
+                            @php $selectedValue_status = old('status') ?: $blog->status @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="status_switch"
+                                id="status_switch"
+                                class="cursor-pointer"
+                                value="1"
+                                :label="trans('blog::app.blog.status')"
+                                :checked="(boolean) $selectedValue_status"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        <!-- Allow Comments -->
+                        <input type="hidden" name="allow_comments" id="allow_comments" value="@php echo $blog->allow_comments @endphp">
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.allow_comments')
+                            </x-admin::form.control-group.label>
+
+                            @php $selectedValue_allow_comments = old('allow_comments') ?: $blog->allow_comments @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="allow_comments_switch"
+                                class="cursor-pointer"
+                                value="1"
+                                :label="trans('blog::app.blog.allow_comments')"
+                                :checked="(boolean) $selectedValue_allow_comments"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        <!-- Auther -->
+                        @php
+
+                        $loggedIn_user = auth()->guard('admin')->user()->toarray();
+                        $user_id = ( array_key_exists('id', $loggedIn_user) ) ? $loggedIn_user['id'] : 0;
+                        $user_name = ( array_key_exists('name', $loggedIn_user) ) ? $loggedIn_user['name'] : '';
+                        $role = ( array_key_exists('role', $loggedIn_user) ) ? ( array_key_exists('name', $loggedIn_user['role']) ? $loggedIn_user['role']['name'] : 'Administrator' ) : 'Administrator';
+
+                        @endphp
+
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required text-gray-800 dark:text-white font-medium required">
+                                @lang('blog::app.blog.author')
+                            </x-admin::form.control-group.label>
+
+                            @if( $role != 'Administrator' )
+                                <input type="hidden" name="author_id" id="author_id" value="{{$user_id}}">
+                                <x-admin::form.control-group.control
+                                    type="text"
+                                    name="author"
+                                    rules="required"
+                                    disabled="disabled"
+                                    :value="$user_name"
+                                    :label="trans('blog::app.blog.author')"
+                                    :placeholder="trans('blog::app.blog.author')"
+                                >
+                                </x-admin::form.control-group.control>
+                            @else
+                                <x-admin::form.control-group.control
+                                    type="select"
+                                    name="author_id"
+                                    id="author_id"
+                                    {{-- class="cursor-pointer" --}}
+                                    rules="required"
+                                    :value="old('author_id') ?? $blog->author_id"
+                                    :label="trans('blog::app.blog.author')"
+                                    {{-- :placeholder="trans('blog::app.blog.author')" --}}
+                                >
+                                    <!-- Options -->
+                                    <option value="">Select an author</option>
+                                    @foreach($users as $user)
+                                        <option value="{{$user->id}}">{{$user->name}}</option>
+                                    @endforeach
+                                </x-admin::form.control-group.control>
+
+                                <x-admin::form.control-group.error
+                                    control-name="author"
+                                >
+                                </x-admin::form.control-group.error>
+                            @endif
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                <!-- Default Categories -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="required p-2.5 text-gray-800 dark:text-gray-300 text-base font-semibold">
+                            {{-- @lang('blog::app.blog.categories_title') --}}
+                            Default Category
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Category -->
+                        <x-admin::form.control-group class="mb-2.5">
+
+                            <x-admin::form.control-group.control
+                                type="select"
+                                name="default_category"
+                                id="default_category"
+                                {{-- class="cursor-pointer" --}}
+                                rules="required"
+                                :value="old('default_category') ?? $blog->default_category"
+                                :label="trans('blog::app.blog.default_category')"
+                            >
+                                <!-- Options -->
+                                <option value="">Select an category</option>
+                                @foreach($categories as $category)
+                                    <option value="{{$category->id}}" data-slug="{{$category->slug}}" id="{{'default_category'.$category->id}}" {{ $blog->default_category == $category->id ? 'selected' : '' }} >{{$category->name}}</option>
+                                @endforeach
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="default_category"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                <!-- Additional Category -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-800 dark:text-gray-300 text-base font-semibold">
+                            {{-- @lang('blog::app.blog.categories_title') --}}
+                            Additional Category
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <div class="flex flex-col gap-[12px]">
+                            <x-admin::tree.view
+                                input-type="checkbox"
+                                name-field="categorys"
+                                id-field="id"
+                                value-field="id"
+                                :items="json_encode($additional_categories)"
+                                :value="json_encode(explode(',', $blog->categorys))"
+                                behavior="no"
+                                :fallback-locale="config('app.fallback_locale')"
+                            >
+                            </x-admin::tree.view>
+                        </div>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                <!-- Tags -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="required p-2.5 text-gray-800 dark:text-gray-300 text-base font-semibold">
+                            @lang('blog::app.blog.tag_title')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+                        @foreach ($tags as $tag)
+                            <x-admin::form.control-group class="flex gap-2.5 !mb-0 p-1.5">
+                                <x-admin::form.control-group.control
+                                    type="checkbox"
+                                    name="tags[]"
+                                    :id="$tag->name"
+                                    :value="$tag->id"
+                                    rules="required"
+                                    :for="$tag->name"
+                                    :label="trans('blog::app.blog.tags')"
+                                    :checked="in_array($tag->id, explode(',', $blog->tags))"
+                                >
+                                </x-admin::form.control-group.control>
+
+                                <x-admin::form.control-group.label
+                                    :for="$tag->name"
+                                    class="!text-sm !text-gray-600 dark:!text-gray-300 font-semibold cursor-pointer"
+                                >
+                                    {{ $tag->name }}
+                                </x-admin::form.control-group.label>
+                            </x-admin::form.control-group>
+                        @endforeach
+
+                        <x-admin::form.control-group.error
+                            control-name="tags[]"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                {!! view_render_event('admin.blogs.edit.after', ['blogs' => $blog]) !!}
+
+            </div>
+        </div>
+
+    </x-admin::form>
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-seo-helper-custom-template">
+        <div class="flex flex-col gap-[3px] mb-[30px]">
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                v-text="metaTitle"
+            >
+            </p>
+
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                style="display: none;"
+                v-text="metaSlug"
+            >
+            </p>
+
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                style="display: none;"
+                v-text="metaSlugCategory"
+            >
+            </p>
+
+            <!-- SEO Meta Title -->
+            <p 
+                class="text-[#135F29]"
+                v-text="'{{ URL::to('/') }}/blog' + ( ( metaSlugCategory != '' && metaSlugCategory != null && metaSlugCategory != undefined ) ? '/'+metaSlugCategory : '' ) + '/' + (metaSlug ? metaSlug.toLowerCase().replace(/\s+/g, '-') : '')"
+            >
+            </p>
+
+            <!-- SEP Meta Description -->
+            <p 
+                class="text-gray-600 dark:text-gray-300"
+                v-text="metaDescription"
+            >
+            </p>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-seo-helper-custom', {
+            template: '#v-seo-helper-custom-template',
+
+            data() {
+                return {
+                    metaTitle: this.$parent.getValues()['meta_title'],
+
+                    metaDescription: this.$parent.getValues()['meta_description'],
+
+                    metaSlug: this.$parent.getValues()['slug'],
+
+                    metaSlugCategory: this.$parent.getValues()['default_category'],
+                }
+            },
+
+            mounted() {
+                let self = this;
+
+                self.metaTitle = document.getElementById('meta_title').value;
+
+                self.metaDescription = document.getElementById('meta_description').value;
+
+                self.metaSlug = document.getElementById('slug').value;
+
+                var d_cat_id = document.getElementById('default_category').value;
+
+                var d_cat_slug = document.getElementById('default_category' + d_cat_id).getAttribute("data-slug");
+
+                self.metaSlugCategory = ( d_cat_slug != '' && d_cat_slug != null && d_cat_slug != undefined ) ? d_cat_slug : '';
+
+                document.getElementById('meta_title').addEventListener('input', function(e) {
+                    self.metaTitle = e.target.value;
+                });
+
+                document.getElementById('meta_description').addEventListener('input', function(e) {
+                    self.metaDescription = e.target.value;
+                });
+
+                document.getElementById('name').addEventListener('input', function(e) {
+                    setTimeout(function(){
+                        var slug = document.getElementById('slug').value;
+                        self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                    }, 1000);
+                });
+
+                document.getElementById('slug').addEventListener('input', function(e) {
+                    var slug = e.target.value;
+                    self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                });
+
+                document.getElementById('default_category').addEventListener('change', function(e) {
+                    var cat_slug = document.getElementById('default_category' + e.target.value).getAttribute("data-slug");
+                    self.metaSlugCategory = ( cat_slug != '' && cat_slug != null && cat_slug != undefined ) ? cat_slug : '';
+                });
+
+                document.getElementById('status_switch').addEventListener('change', function(e) {
+                    document.getElementById('status').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+
+                document.getElementById('allow_comments_switch').addEventListener('change', function(e) {
+                    document.getElementById('allow_comments').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+
+            },
+        });
+    </script>
+
+@endPushOnce
+
+</x-admin::layouts>

+ 28 - 0
packages/Longyi/Blog/src/Resources/views/admin/blogs/index.blade.php

@@ -0,0 +1,28 @@
+<x-admin::layouts>
+    <x-slot:title>
+        {{ __('Blogs') }}
+    </x-slot:title>
+
+    <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+        <p class="text-xl text-gray-800 dark:text-white font-bold">
+            {{ __('Blogs') }}
+        </p>
+
+        <div class="flex gap-x-2.5 items-center">
+            @if (bouncer()->hasPermission('blog.blogs.create'))
+                <a href="{{ route('admin.blog.create') }}">
+                    <div class="primary-button">
+                        {{ __('Create Blog') }}
+                    </div>
+                </a>
+            @endif
+        </div>        
+    </div>
+
+    {!! view_render_event('bagisto.admin.catalog.categories.list.before') !!}
+
+    <x-admin::datagrid src="{{ route('admin.blog.index') }}"></x-admin::datagrid>
+
+    {!! view_render_event('bagisto.admin.catalog.categories.list.after') !!}
+
+</x-admin::layouts>

+ 416 - 0
packages/Longyi/Blog/src/Resources/views/admin/categories/create.blade.php

@@ -0,0 +1,416 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('blog::app.category.add-title')
+    </x-slot:title>
+
+    @pushOnce('styles')
+
+        <style type="text/css">
+            
+            .v-tree-container>.v-tree-item:not(.has-children) {
+                padding-left: 18px !important;
+            }
+            
+        </style>
+
+    @endPushOnce
+
+    @php
+        $currentLocale = core()->getRequestedLocale();
+    @endphp
+    
+    <!-- Blog Edit Form -->
+    <x-admin::form
+        :action="route('admin.blog.category.store')"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        {!! view_render_event('admin.blog.categories.create.before') !!}
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                @lang('blog::app.category.add-title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <!-- Cancel Button -->
+                <a
+                    href="{{ route('admin.blog.category.index') }}"
+                    class="transparent-button hover:bg-gray-200 dark:hover:bg-gray-800 dark:text-white"
+                >
+                    @lang('admin::app.catalog.categories.create.back-btn')
+                </a>
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                >
+                    @lang('blog::app.category.create-btn-title')
+                </button>
+            </div>
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <!-- Left Section -->
+            <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+
+                <!-- General -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('admin::app.catalog.categories.create.general')
+                    </p>
+
+                    <!-- Locales -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="locale"
+                        value="{{ core()->getRequestedLocaleCode() }}"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Name -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.category.name')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="name"
+                            value="{{ old('name') }}"
+                            label="{{ trans('blog::app.category.name') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="name"
+                                id="name"
+                                v-bind="field"
+                                :class="[errors['{{ 'name' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.category.name') }}"
+                                v-slugify-target:slug="setValues"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="name"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Slug -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.category.slug')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="slug"
+                            value="{{ old('slug') }}"
+                            label="{{ trans('blog::app.category.slug') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="slug"
+                                id="slug"
+                                v-bind="field"
+                                :class="[errors['{{ 'slug' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.category.slug') }}"
+                                v-slugify-target:slug
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="slug"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                </div>
+
+                <!-- Description and images -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.blog.description-and-images')
+                    </p>
+
+                    <!-- Description -->
+                    <v-description>
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.category.description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="description"
+                                id="description"
+                                class="description"
+                                rules="required"
+                                :value="old('description')"
+                                :label="trans('blog::app.category.description')"
+                                :tinymce="true"
+                                :prompt="core()->getConfigData('general.magic_ai.content_generation.category_description_prompt')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="description"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+                    </v-description>
+
+                    <div class="flex gap-12">
+                        <!-- Add Logo -->
+                        <div class="flex flex-col gap-2 w-2/5 mt-5">
+                            <p class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.category.image')
+                            </p>
+
+                            <x-admin::media.images name="image" rules="required"></x-admin::media.images>
+
+                            <x-admin::form.control-group.error control-name="image"></x-admin::form.control-group.error>
+
+                        </div>
+
+                    </div>
+                </div>
+
+                <!-- SEO Deatils -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.blog.search_engine_optimization')
+                    </p>
+
+                    <!-- SEO Title & Description Blade Componnet -->
+                    {{-- <x-admin::seo/> --}}
+                    <v-seo-helper-custom></v-seo-helper-custom>
+
+                    <div class="mt-8">
+                        <!-- Meta Title -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_title')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_title"
+                                id="meta_title"
+                                rules="required"
+                                :value="old('meta_title')"
+                                :label="trans('blog::app.blog.meta_title')"
+                                :placeholder="trans('blog::app.blog.meta_title')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_title"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Keywords -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_keywords')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_keywords"
+                                rules="required"
+                                :value="old('meta_keywords')"
+                                :label="trans('blog::app.blog.meta_keywords')"
+                                :placeholder="trans('blog::app.blog.meta_keywords')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_keywords"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Description -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="meta_description"
+                                id="meta_description"
+                                rules="required"
+                                :value="old('meta_description')"
+                                :label="trans('blog::app.blog.meta_description')"
+                                :placeholder="trans('blog::app.blog.meta_description')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_description"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+                    </div>
+                </div>
+
+            </div>
+
+            <!-- Right Section -->
+            <div class="flex flex-col gap-2 w-[360px] max-w-full">
+
+                <!-- Settings -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-600 dark:text-gray-300 text-base font-semibold">
+                            @lang('admin::app.catalog.categories.create.settings')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.status')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="status"
+                                class="cursor-pointer"
+                                value="1"
+                                :label="trans('blog::app.blog.status')"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                <!-- Parent Category -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-600 dark:text-gray-300 text-base font-semibold">
+                            {{-- @lang('admin::app.catalog.categories.create.settings') --}}
+                            Parent Category
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <div class="flex flex-col gap-[12px]">
+                            <x-admin::tree.view
+                                input-type="radio"
+                                name-field="parent_id"
+                                value-field="key"
+                                id-field="key"
+                                model-value="json_encode($categories)"
+                                :items="json_encode($categories)"
+                                :fallback-locale="config('app.fallback_locale')"
+                            >
+                            </x-admin::tree.view>
+                        </div>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                {!! view_render_event('admin.blog.categories.create.after') !!}
+
+            </div>
+        </div>
+
+    </x-admin::form>
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-seo-helper-custom-template">
+        <div class="flex flex-col gap-[3px] mb-[30px]">
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                v-text="metaTitle"
+            >
+            </p>
+
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                style="display: none;"
+                v-text="metaSlug"
+            >
+            </p>
+
+            <!-- SEO Meta Title -->
+            <p 
+                class="text-[#135F29]"
+                v-text="'{{ URL::to('/') }}/blog/' + (metaSlug ? metaSlug.toLowerCase().replace(/\s+/g, '-') : '')"
+            >
+            </p>
+
+            <!-- SEP Meta Description -->
+            <p 
+                class="text-gray-600 dark:text-gray-300"
+                v-text="metaDescription"
+            >
+            </p>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-seo-helper-custom', {
+            template: '#v-seo-helper-custom-template',
+
+            data() {
+                return {
+                    metaTitle: this.$parent.getValues()['meta_title'],
+
+                    metaDescription: this.$parent.getValues()['meta_description'],
+
+                    metaSlug: this.$parent.getValues()['slug'],
+                }
+            },
+
+            mounted() {
+                let self = this;
+
+                self.metaTitle = document.getElementById('meta_title').value;
+
+                self.metaDescription = document.getElementById('meta_description').value;
+
+                self.metaSlug = document.getElementById('slug').value;
+
+                document.getElementById('meta_title').addEventListener('input', function(e) {
+                    self.metaTitle = e.target.value;
+                });
+
+                document.getElementById('meta_description').addEventListener('input', function(e) {
+                    self.metaDescription = e.target.value;
+                });
+
+                document.getElementById('name').addEventListener('input', function(e) {
+                    setTimeout(function(){
+                        var slug = document.getElementById('slug').value;
+                        self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                    }, 1000);
+                });
+
+                document.getElementById('slug').addEventListener('input', function(e) {
+                    var slug = e.target.value;
+                    self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                });
+
+            },
+        });
+    </script>
+
+@endPushOnce
+
+</x-admin::layouts>

+ 456 - 0
packages/Longyi/Blog/src/Resources/views/admin/categories/edit.blade.php

@@ -0,0 +1,456 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('blog::app.category.edit-title')
+    </x-slot:title>
+
+    @pushOnce('styles')
+
+        <style type="text/css">
+            
+            .v-tree-container>.v-tree-item:not(.has-children) {
+                padding-left: 18px !important;
+            }
+            
+        </style>
+
+    @endPushOnce
+
+    @php
+        $locale = core()->getRequestedLocaleCode();
+        $currentLocale = core()->getRequestedLocale();
+    @endphp
+    
+    <!-- Blog Edit Form -->
+    <x-admin::form
+        :action="route('admin.blog.category.update', $categories->id)"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        {!! view_render_event('admin.blog.categories.edit.before') !!}
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                @lang('blog::app.category.edit-title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <!-- Back Button -->
+                <a
+                    href="{{ route('admin.blog.category.index') }}"
+                    class="transparent-button hover:bg-gray-200 dark:hover:bg-gray-800 dark:text-white"
+                >
+                    @lang('admin::app.catalog.categories.edit.back-btn')
+                </a>
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                >
+                    @lang('blog::app.category.create-btn-title')
+                </button>
+            </div>
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <!-- Left Section -->
+            <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+
+                <!-- General -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('admin::app.catalog.categories.create.general')
+                    </p>
+
+                    <!-- Locales -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="locale"
+                        value="{{ core()->getRequestedLocaleCode() }}"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Name -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.category.name')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="name"
+                            value="{{ old('name') ?? $categories->name }}"
+                            label="{{ trans('blog::app.category.name') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="name"
+                                id="name"
+                                v-bind="field"
+                                :class="[errors['{{ 'name' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.category.name') }}"
+                                v-slugify-target:slug="setValues"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="name"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Slug -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.category.slug')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="slug"
+                            value="{{ old('slug') ?? $categories->slug }}"
+                            label="{{ trans('blog::app.category.slug') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="slug"
+                                id="slug"
+                                v-bind="field"
+                                :class="[errors['{{ 'slug' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.category.slug') }}"
+                                v-slugify-target:slug
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="slug"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                </div>
+
+                <!-- Description and images -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.blog.description-and-images')
+                    </p>
+
+                    <!-- Description -->
+                    <v-description>
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.category.description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="description"
+                                id="description"
+                                class="description"
+                                rules="required"
+                                :value="old('description') ?? $categories->description"
+                                :label="trans('blog::app.category.description')"
+                                :tinymce="true"
+                                :prompt="core()->getConfigData('general.magic_ai.content_generation.category_description_prompt')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="description"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+                    </v-description>
+
+                    <div class="flex gap-12">
+                        <!-- Add Logo -->
+                        <div class="flex flex-col gap-2 w-2/5 mt-5">
+                            <p class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.category.image')
+                            </p>
+
+                            <x-admin::media.images 
+                                name="image" 
+                                :uploaded-images="$categories->image ? [['id' => 'image', 'url' => $categories->image_url]] : []"
+                            >
+                                
+                            </x-admin::media.images>
+
+                        </div>
+
+                    </div>
+                </div>
+
+                <!-- SEO Deatils -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.blog.search_engine_optimization')
+                    </p>
+
+                    <!-- SEO Title & Description Blade Componnet -->
+                    {{-- <x-admin::seo/> --}}
+                    <v-seo-helper-custom></v-seo-helper-custom>
+
+                    <div class="mt-8">
+                        <!-- Meta Title -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_title')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_title"
+                                id="meta_title"
+                                rules="required"
+                                :value="old('meta_title') ?? $categories->meta_title"
+                                :label="trans('blog::app.blog.meta_title')"
+                                :placeholder="trans('blog::app.blog.meta_title')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_title"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Keywords -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_keywords')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_keywords"
+                                rules="required"
+                                :value="old('meta_keywords') ?? $categories->meta_keywords"
+                                :label="trans('blog::app.blog.meta_keywords')"
+                                :placeholder="trans('blog::app.blog.meta_keywords')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_keywords"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Description -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="meta_description"
+                                id="meta_description"
+                                rules="required"
+                                :value="old('meta_description') ?? $categories->meta_description"
+                                :label="trans('blog::app.blog.meta_description')"
+                                :placeholder="trans('blog::app.blog.meta_description')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_description"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+                    </div>
+                </div>
+
+            </div>
+
+            <!-- Right Section -->
+            <div class="flex flex-col gap-2 w-[360px] max-w-full">
+                <!-- Settings -->
+
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-600 dark:text-gray-300 text-base font-semibold">
+                            @lang('admin::app.catalog.categories.create.settings')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <v-setting-custom></v-setting-custom>
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.blog.status')
+                            </x-admin::form.control-group.label>
+
+                            @php $selectedValue_status = old('status') ?: $categories->status @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="status_switch"
+                                id="status_switch"
+                                class="cursor-pointer"
+                                value="1"
+                                :label="trans('blog::app.blog.status')"
+                                :checked="(boolean) $selectedValue_status"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                <!-- Parent Category -->
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-600 dark:text-gray-300 text-base font-semibold">
+                            {{-- @lang('admin::app.catalog.categories.create.settings') --}}
+                            Parent Category
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <div class="flex flex-col gap-[12px]">
+                            <x-admin::tree.view
+                                input-type="radio"
+                                name-field="parent_id"
+                                value-field="key"
+                                id-field="key"
+                                :value="json_encode($categories->parent_id)"
+                                :value-field="json_encode($categories->parent_id)"
+                                :model-value="json_encode($categories_data)"
+                                :items="json_encode($categories_data)"
+                                :fallback-locale="config('app.fallback_locale')"
+                            >
+                            </x-admin::tree.view>
+                        </div>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                {!! view_render_event('admin.blog.categories.edit.after', ['category' => $categories]) !!}
+
+            </div>
+        </div>
+
+    </x-admin::form>
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-setting-custom-template">
+        <input type="hidden" name="status" id="status" value="@php echo $categories->status @endphp">
+    </script>
+
+    <script type="module">
+        app.component('v-setting-custom', {
+            template: '#v-setting-custom-template',
+
+            data() {
+                return {
+                    
+                }
+            },
+
+            mounted() {
+                let self = this;
+
+                document.getElementById('status_switch').addEventListener('change', function(e) {
+                    document.getElementById('status').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+
+            },
+        });
+    </script>
+
+@endPushOnce
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-seo-helper-custom-template">
+        <div class="flex flex-col gap-[3px] mb-[30px]">
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                v-text="metaTitle"
+            >
+            </p>
+
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                style="display: none;"
+                v-text="metaSlug"
+            >
+            </p>
+
+            <!-- SEO Meta Title -->
+            <p 
+                class="text-[#135F29]"
+                v-text="'{{ URL::to('/') }}/blog/' + (metaSlug ? metaSlug.toLowerCase().replace(/\s+/g, '-') : '')"
+            >
+            </p>
+
+            <!-- SEP Meta Description -->
+            <p 
+                class="text-gray-600 dark:text-gray-300"
+                v-text="metaDescription"
+            >
+            </p>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-seo-helper-custom', {
+            template: '#v-seo-helper-custom-template',
+
+            data() {
+                return {
+                    metaTitle: this.$parent.getValues()['meta_title'],
+
+                    metaDescription: this.$parent.getValues()['meta_description'],
+
+                    metaSlug: this.$parent.getValues()['slug'],
+                }
+            },
+
+            mounted() {
+                let self = this;
+
+                self.metaTitle = document.getElementById('meta_title').value;
+
+                self.metaDescription = document.getElementById('meta_description').value;
+
+                self.metaSlug = document.getElementById('slug').value;
+
+                document.getElementById('meta_title').addEventListener('input', function(e) {
+                    self.metaTitle = e.target.value;
+                });
+
+                document.getElementById('meta_description').addEventListener('input', function(e) {
+                    self.metaDescription = e.target.value;
+                });
+
+                document.getElementById('name').addEventListener('input', function(e) {
+                    setTimeout(function(){
+                        var slug = document.getElementById('slug').value;
+                        self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                    }, 1000);
+                });
+
+                document.getElementById('slug').addEventListener('input', function(e) {
+                    var slug = e.target.value;
+                    self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                });
+
+            },
+        });
+    </script>
+
+@endPushOnce
+
+</x-admin::layouts>

+ 28 - 0
packages/Longyi/Blog/src/Resources/views/admin/categories/index.blade.php

@@ -0,0 +1,28 @@
+<x-admin::layouts>
+    <x-slot:title>
+        {{ __('Blog Categories') }}
+    </x-slot:title>
+
+    <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+        <p class="text-xl text-gray-800 dark:text-white font-bold">
+            {{ __('Blog Categories') }}
+        </p>
+
+        <div class="flex gap-x-2.5 items-center">
+            @if (bouncer()->hasPermission('blog.category.create'))
+                <a href="{{ route('admin.blog.category.create') }}">
+                    <div class="primary-button">
+                        {{ __('Create Category') }}
+                    </div>
+                </a>
+            @endif
+        </div>        
+    </div>
+
+    {!! view_render_event('bagisto.admin.catalog.categories.list.before') !!}
+
+    <x-admin::datagrid src="{{ route('admin.blog.category.index') }}"></x-admin::datagrid>
+
+    {!! view_render_event('bagisto.admin.catalog.categories.list.after') !!}
+
+</x-admin::layouts>

+ 240 - 0
packages/Longyi/Blog/src/Resources/views/admin/comments/edit.blade.php

@@ -0,0 +1,240 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('blog::app.comment.edit-title')
+    </x-slot:title>
+
+    @php
+        $currentLocale = core()->getRequestedLocale();
+    @endphp
+    
+    <!-- Blog Edit Form -->
+    <x-admin::form
+        :action="route('admin.blog.comment.update', $comment->id)"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        {!! view_render_event('admin.blog.comments.edit.before') !!}
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                @lang('blog::app.comment.edit-title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <!-- Back Button -->
+                <a
+                    href="{{ route('admin.blog.tag.index') }}"
+                    class="transparent-button hover:bg-gray-200 dark:hover:bg-gray-800 dark:text-white"
+                >
+                    @lang('admin::app.catalog.categories.edit.back-btn')
+                </a>
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                >
+                    @lang('blog::app.comment.edit-btn-title')
+                </button>
+            </div>
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <!-- Left Section -->
+            <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+
+                <!-- General -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded-[4px] box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('admin::app.catalog.categories.create.general')
+                    </p>
+
+                    <!-- Locales -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="locale"
+                        value="{{ core()->getRequestedLocaleCode() }}"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Author ID -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="author"
+                        :value="$comment->author"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Name -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.comment.post')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="post"
+                            value="{{ old('post') ?? $comment->blog->name }}"
+                            label="{{ trans('blog::app.comment.post') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                            disabled="disabled"
+                        >
+                            <input
+                                type="text"
+                                name="post"
+                                id="post"
+                                v-bind="field"
+                                :class="[errors['{{ 'post' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.comment.post') }}"
+                                v-slugify-target:slug="setValues"
+                                disabled="disabled"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="post"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Slug -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.comment.name')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="author_name"
+                            {{-- value="{{ old('author_name') ?? $author_name }}" --}}
+                            value="{{ old('author_name') ?? $comment->name }}"
+                            label="{{ trans('blog::app.comment.name') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                            disabled="disabled"
+                        >
+                            <input
+                                type="text"
+                                name="author_name"
+                                id="author_name"
+                                v-bind="field"
+                                :class="[errors['{{ 'author_name' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.comment.name') }}"
+                                v-slugify-target:slug
+                                disabled="disabled"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="author_name"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Published At -->
+                    <x-admin::form.control-group class="w-full mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.comment.comment_date')
+                        </x-admin::form.control-group.label>
+
+                        <x-admin::form.control-group.control
+                            type="date"
+                            name="created_at" 
+                            id="created_at"
+                            rules="required"
+                            disabled="disabled"
+                            :value="old('created_at') ?? date_format(date_create($comment->created_at),'Y-m-d')"
+                            :label="trans('blog::app.comment.comment_date')"
+                            :placeholder="trans('blog::app.comment.comment_date')"
+                        >
+                        </x-admin::form.control-group.control>
+
+                        <x-admin::form.control-group.error
+                            control-name="created_at"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Description -->
+                    <v-description>
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.comment.comment')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="comment"
+                                id="comment"
+                                class="comment"
+                                rules="required"
+                                :value="old('comment') ?? $comment->comment"
+                                :label="trans('blog::app.comment.comment')"
+                                :tinymce="true"
+                                :prompt="core()->getConfigData('general.magic_ai.content_generation.category_description_prompt')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="comment"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+                    </v-description>
+
+                </div>
+
+            </div>
+
+            <!-- Right Section -->
+            <div class="flex flex-col gap-2 w-[360px] max-w-full">
+                <!-- Settings -->
+
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-600 dark:text-gray-300 text-base font-semibold">
+                            @lang('admin::app.catalog.categories.create.settings')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.comment.status')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="select"
+                                name="status"
+                                id="status"
+                                {{-- class="cursor-pointer" --}}
+                                :value="old('status') ?? $comment->status"
+                                :label="trans('blog::app.comment.status')"
+                            >
+                                <!-- Options -->
+                                @foreach($status_details as $status_data)
+                                    <option value="{{$status_data['id']}}" {{ $comment->status == $status_data['id'] ? 'selected' : '' }} >@lang($status_data['name'])</option>
+                                @endforeach
+                            </x-admin::form.control-group.control>
+
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                {!! view_render_event('admin.blog.comments.edit.after', ['comment' => $comment]) !!}
+
+            </div>
+        </div>
+
+    </x-admin::form>
+
+</x-admin::layouts>

+ 28 - 0
packages/Longyi/Blog/src/Resources/views/admin/comments/index.blade.php

@@ -0,0 +1,28 @@
+<x-admin::layouts>
+    <x-slot:title>
+        {{ __('Blog Comments') }}
+    </x-slot:title>
+
+    <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+        <p class="text-xl text-gray-800 dark:text-white font-bold">
+            {{ __('Blog Comments') }}
+        </p>
+
+        {{-- <div class="flex gap-x-2.5 items-center">
+            @if (bouncer()->hasPermission('catalog.categories.create'))
+                <a href="{{ route('admin.blog.category.create') }}">
+                    <div class="primary-button">
+                        @lang('admin::app.catalog.categories.index.add-btn')
+                    </div>
+                </a>
+            @endif
+        </div>   --}}      
+    </div>
+
+    {!! view_render_event('bagisto.admin.catalog.categories.list.before') !!}
+
+    <x-admin::datagrid src="{{ route('admin.blog.comment.index') }}"></x-admin::datagrid>
+
+    {!! view_render_event('bagisto.admin.catalog.categories.list.after') !!}
+
+</x-admin::layouts>

+ 269 - 0
packages/Longyi/Blog/src/Resources/views/admin/import/index.blade.php

@@ -0,0 +1,269 @@
+<x-admin::layouts>
+    <x-slot:title>
+        {{ __('Blog Import/Export') }}
+    </x-slot:title>
+
+    @pushOnce('styles')
+
+        <style type="text/css">
+            
+            .w-50 {
+                width: calc(50% - 4px);
+            }
+            @media (max-width: 767px) {
+                .w-50 {
+                    width: 100%;
+                }  
+                .flex-col-box {
+                    flex-direction: column;
+                }
+            }
+
+            .main-card .card .card-header {
+                background: #c8c1b645;
+                color: #fff;
+                padding: 14px 20px !important;
+            }
+
+            .main-card .card .card-header h4{
+                color: #000;
+            }
+
+            .main-card .card {
+                border-color: #c8c1b645;
+            }
+
+            .main-card .card .card-body span {
+                color: #000;
+            }
+
+            .main-card .card-header h4 {
+                color: red;
+            }
+
+            .sub-card .card-body span:hover {
+                background-color: #fd625e;
+                cursor: auto;
+            }
+
+            .error-text {
+                display: flex;
+                cursor: pointer;
+                align-items: center;
+                -moz-column-gap: .25rem;
+                column-gap: .25rem;
+                border-radius: .375rem;
+                border-width: 1px;
+                border-color: #fd625e;
+                background-color: #ffffff;
+                padding: .375rem .75rem;
+                color: #fd625e !important;
+                transition-property: all;
+                transition-timing-function: cubic-bezier(.4,0,.2,1);
+                transition-duration: .15s;
+                font-weight: 400;
+                font-size: smaller;
+            }
+            .error-text:hover {
+                display: flex;
+                cursor: pointer;
+                align-items: center;
+                -moz-column-gap: .25rem;
+                column-gap: .25rem;
+                border-radius: .375rem;
+                border-width: 1px;
+                border-color: #fd625e;
+                background-color: #ffffff;
+                padding: .375rem .75rem;
+                color: #fff !important;
+                transition-property: all;
+                transition-timing-function: cubic-bezier(.4,0,.2,1);
+                transition-duration: .15s;
+                font-weight: 400;
+                font-size: smaller;
+            }
+
+            @media (max-width: 1366px) {
+                .wc-grid-responsive {
+                    grid-template-columns: repeat(3, 1fr);
+                }
+            }
+
+            @media (max-width: 768px) {
+                .wc-grid-responsive {
+                    grid-template-columns: repeat(2, 1fr);
+                }
+            }
+
+            @media (max-width: 575px) {
+                .wc-grid-responsive {
+                    grid-template-columns: repeat(1, 1fr);
+                }
+            }
+
+        </style>
+
+    @endPushOnce
+
+    <!-- Blog Import/Export Form -->
+    <x-admin::form
+        :action="route('admin.blog.import.store')"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                {{ __('Blog Import/Export') }}
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+
+                <a 
+                    href="{{ route( 'admin.blog.export.download' ) }}" 
+                    target="_blank" 
+                    class="transparent-button hover:bg-gray-200 dark:hover:bg-gray-800 dark:text-white"
+                ><span class="icon-export text-xl text-gray-600"></span> Export </a>
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                ><span class="icon-import text-xl text-white"></span>Import</button>
+            </div>
+
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <!-- Import Section -->
+            <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+            
+                <!-- Import Section -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        {{ __( 'Import' ) }}
+                    </p>
+
+                    <!-- Name -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            {{ __( 'Import File' ) }}
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="file"
+                            name="imoprt_file"
+                            value="{{ old('imoprt_file') }}"
+                            label="{{ __( 'Import File' ) }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="file"
+                                name="imoprt_file"
+                                id="imoprt_file"
+                                v-bind="field"
+                                :class="[errors['{{ 'imoprt_file' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ __( 'Import File' ) }}"
+                                accept=".csv"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="imoprt_file"
+                        >
+                        </x-admin::form.control-group.error>
+                        <span class="text-xs">You can upload only csv file and download sample csv file here. <a href="{{ env('APP_URL') . '/storage/blog-sample/sample.csv' }}" target="_blank" class="text-blue-600">Click Here</a></span>
+                    </x-admin::form.control-group>
+
+                    @if( count( $errors_data ) > 0 )
+                        <div class="card main-card bg-white rounded box-shadow">
+                            <div class="card-header p-4 font-semibold" style="background-color: antiquewhite;">
+                                <h4 class="card-title mb-0">Import Errors</h4>
+                            </div>
+                            <div class="card-body p-4 mb-4 w-full" style="height: auto; overflow-y: auto; max-height: 550px;">
+                                <div class="grid grid-cols-4 gap-2 wc-grid-responsive">
+                                    @foreach( $errors_data as $error_data )
+                                        <div class="col-lg-4 bg-white rounded box-shadow">
+                                            <div class="card sub-card">
+                                                <div class="card-header">
+                                                    <h4 class="card-title mb-0">{{ $error_data[ 'line_no' ] }}</h4>
+                                                </div>
+                                                <div class="card-body p-4" style="height: auto; overflow-y: auto; max-height: 200px;">
+                                                    @foreach( $error_data[ 'errors' ] as $error )
+                                                        @if( isset( $error ) && !empty( $error ) && !is_null( $error ) )
+                                                            <span class="error-text mb-2">{{ $error }}</span>
+                                                        @endif
+                                                    @endforeach
+                                                </div>
+                                            </div>  
+                                        </div>
+                                    @endforeach
+                                </div>
+                            </div>
+                        </div>
+                    @endif
+
+                </div>
+
+            </div>
+
+
+        </div>
+
+        <v-wc-custom-js></v-wc-custom-js>
+
+    </x-admin::form>
+
+@pushOnce('scripts')
+    
+    <script type="text/x-template" id="v-wc-custom-js-template">
+        
+    </script>
+
+    <script type="module">
+        app.component('v-wc-custom-js', {
+            template: '#v-wc-custom-js-template',
+
+            data() {
+                return {
+                    
+                }
+            },
+
+            mounted() {
+                let self = this;
+                
+                // document.getElementById('switch_blog_post_show_categories_with_count').addEventListener('change', function(e) {
+                //     document.getElementById('blog_post_show_categories_with_count').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                // });
+                
+                // document.getElementById('switch_blog_post_show_tags_with_count').addEventListener('change', function(e) {
+                //     document.getElementById('blog_post_show_tags_with_count').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                // });
+                
+                // document.getElementById('switch_blog_post_show_author_page').addEventListener('change', function(e) {
+                //     document.getElementById('blog_post_show_author_page').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                // });
+                
+                // document.getElementById('switch_blog_post_enable_comment').addEventListener('change', function(e) {
+                //     document.getElementById('blog_post_enable_comment').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                // });
+                
+                // document.getElementById('switch_blog_post_allow_guest_comment').addEventListener('change', function(e) {
+                //     document.getElementById('blog_post_allow_guest_comment').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                // });
+                
+                // document.getElementById('switch_blog_post_enable_comment_moderation').addEventListener('change', function(e) {
+                //     document.getElementById('blog_post_enable_comment_moderation').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                // });
+
+            },
+        });
+    </script>
+@endPushOnce
+
+</x-admin::layouts>

+ 417 - 0
packages/Longyi/Blog/src/Resources/views/admin/setting/index.blade.php

@@ -0,0 +1,417 @@
+<x-admin::layouts>
+    <x-slot:title>
+        {{ __('Blog Setting') }}
+    </x-slot:title>
+
+    @pushOnce('styles')
+
+        <style type="text/css">
+            
+            .w-50 {
+                width: calc(50% - 4px);
+            }
+            @media (max-width: 767px) {
+              .w-50 {
+                width: 100%;
+            }  
+            .flex-col-box {
+                flex-direction: column;
+            }
+        }
+        </style>
+
+    @endPushOnce
+
+    <!-- Blog Setting Form -->
+    <x-admin::form
+        :action="route('admin.blog.setting.store')"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        {!! view_render_event('admin.blogs.setting.before') !!}
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                {{ __('Blog Setting') }}
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                >Save Setting</button>
+            </div>
+
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <div class="flex flex-wrap flex-col-box gap-2 flex-1 max-xl:flex-auto">
+            
+                <!-- Post Setting Section -->
+                <div class="p-4 w-50 bg-white dark:bg-gray-900 rounded-[4px] box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        {{ __('Post Setting') }}
+                    </p>
+
+                    <div class="mt-8">
+                        
+                        <!-- Post Per Page Records -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="">
+                                {{ __('Per Page Records') }}
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="number"
+                                name="blog_post_per_page"
+                                id="blog_post_per_page"
+                                {{-- rules="required" --}}
+                                :value="old('blog_post_per_page') ?? $settings['blog_post_per_page']"
+                                label="Per Page Records"
+                                placeholder="Per Page Records"
+                                min="1"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            {{-- <x-admin::form.control-group.error control-name="blog_post_per_page"></x-admin::form.control-group.error> --}}
+
+                        </x-admin::form.control-group>
+                        
+                        <!-- Post Maximum Related Posts Allowed -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="">
+                                {{ __('Maximum Related Posts Allowed') }}
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="number"
+                                name="blog_post_maximum_related"
+                                id="blog_post_maximum_related"
+                                {{-- rules="required" --}}
+                                :value="old('blog_post_maximum_related') ?? $settings['blog_post_maximum_related']"
+                                label="Maximum Related Posts Allowed"
+                                placeholder="Maximum Related Posts Allowed"
+                                min="1"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            {{-- <x-admin::form.control-group.error control-name="blog_post_maximum_related"></x-admin::form.control-group.error> --}}
+
+                        </x-admin::form.control-group>
+                        
+                        <!-- Recent Posts Order By -->
+                        {{-- <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="">
+                                {{ __('Recent Posts Order By') }}
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="select"
+                                name="blog_post_recent_order_by"
+                                id="blog_post_recent_order_by"
+                                :value="old('blog_post_recent_order_by') ?? $settings['blog_post_recent_order_by']"
+                                label="Recent Posts Order By"
+                                placeholder="Recent Posts Order By"
+                                min="1"
+                            >
+                                @foreach($post_orders as $post_order_key => $post_order_val)
+                                    <option value="{{$post_order_key}}">{{$post_order_val}}</option>
+                                @endforeach
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="blog_post_recent_order_by"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group> --}}
+
+                        <!-- Show Categories With Posts Count -->
+                        <input type="hidden" name="blog_post_show_categories_with_count" id="blog_post_show_categories_with_count" value="@php echo $settings['blog_post_show_categories_with_count'] @endphp">
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                {{ __('Show Categories With Posts Count') }}
+                            </x-admin::form.control-group.label>
+
+                            @php $blog_post_show_categories_with_count = old('blog_post_show_categories_with_count') ?: $settings['blog_post_show_categories_with_count'] @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="switch_blog_post_show_categories_with_count"
+                                id="switch_blog_post_show_categories_with_count"
+                                class="cursor-pointer"
+                                value="1"
+                                label="Show Categories With Posts Count"
+                                :checked="(boolean) $blog_post_show_categories_with_count"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        <!-- Show Tags With Posts Count -->
+                        <input type="hidden" name="blog_post_show_tags_with_count" id="blog_post_show_tags_with_count" value="@php echo $settings['blog_post_show_tags_with_count'] @endphp">
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                {{ __('Show Tags With Posts Count') }}
+                            </x-admin::form.control-group.label>
+
+                            @php $blog_post_show_tags_with_count = old('blog_post_show_tags_with_count') ?: $settings['blog_post_show_tags_with_count'] @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="switch_blog_post_show_tags_with_count"
+                                id="switch_blog_post_show_tags_with_count"
+                                class="cursor-pointer"
+                                value="1"
+                                label="Show Tags With Posts Count"
+                                :checked="(boolean) $blog_post_show_tags_with_count"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        <!-- Show Author Page -->
+                        <input type="hidden" name="blog_post_show_author_page" id="blog_post_show_author_page" value="@php echo $settings['blog_post_show_author_page'] @endphp">
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                {{ __('Show Author Page') }}
+                            </x-admin::form.control-group.label>
+
+                            @php $blog_post_show_author_page = old('blog_post_show_author_page') ?: $settings['blog_post_show_author_page'] @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="switch_blog_post_show_author_page"
+                                id="switch_blog_post_show_author_page"
+                                class="cursor-pointer"
+                                value="1"
+                                label="Show Author Page"
+                                :checked="(boolean) $blog_post_show_author_page"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                    </div>
+
+                </div>
+                
+                <!-- Comment Setting Section -->
+                <div class="p-4 w-50 bg-white dark:bg-gray-900 rounded-[4px] box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        {{ __('Comment Setting') }}
+                    </p>
+
+                    <div class="mt-8">
+                        
+                        <!-- Enable Post Comment -->
+                        <input type="hidden" name="blog_post_enable_comment" id="blog_post_enable_comment" value="@php echo $settings['blog_post_enable_comment'] @endphp">
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                {{ __('Enable Post Comment') }}
+                            </x-admin::form.control-group.label>
+
+                            @php $blog_post_enable_comment = old('blog_post_enable_comment') ?: $settings['blog_post_enable_comment'] @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                id="switch_blog_post_enable_comment"
+                                name="switch_blog_post_enable_comment"
+                                class="cursor-pointer"
+                                value="1"
+                                label="Enable Post Comment"
+                                :checked="(boolean) $blog_post_enable_comment"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        <!-- Allow Guest Comment -->
+                        <input type="hidden" name="blog_post_allow_guest_comment" id="blog_post_allow_guest_comment" value="@php echo $settings['blog_post_allow_guest_comment'] @endphp">
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                {{ __('Allow Guest Comment') }}
+                            </x-admin::form.control-group.label>
+
+                            @php $blog_post_allow_guest_comment = old('blog_post_allow_guest_comment') ?: $settings['blog_post_allow_guest_comment'] @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                id="switch_blog_post_allow_guest_comment"
+                                name="switch_blog_post_allow_guest_comment"
+                                class="cursor-pointer"
+                                value="1"
+                                label="Allow Guest Comment"
+                                :checked="(boolean) $blog_post_allow_guest_comment"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        <!-- Enable Comment Moderation -->
+                        {{-- <input type="hidden" name="blog_post_enable_comment_moderation" id="blog_post_enable_comment_moderation" value="@php echo $settings['blog_post_enable_comment_moderation'] @endphp">
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                {{ __('Enable Comment Moderation') }}
+                            </x-admin::form.control-group.label>
+
+                            @php $blog_post_enable_comment_moderation = old('blog_post_enable_comment_moderation') ?: $settings['blog_post_enable_comment_moderation'] @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                id="switch_blog_post_enable_comment_moderation"
+                                name="switch_blog_post_enable_comment_moderation"
+                                class="cursor-pointer"
+                                value="1"
+                                label="Enable Comment Moderation"
+                                :checked="(boolean) $blog_post_enable_comment_moderation"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group> --}}
+
+                        <!-- Allowed maximum nested comment level -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="">
+                                {{ __('Allowed maximum nested comment level') }}
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="number"
+                                name="blog_post_maximum_nested_comment"
+                                id="blog_post_maximum_nested_comment"
+                                {{-- rules="required" --}}
+                                :value="old('blog_post_maximum_nested_comment') ?? $settings['blog_post_maximum_nested_comment']"
+                                label="Allowed maximum nested comment level"
+                                placeholder="Allowed maximum nested comment level"
+                                min="2"
+                                max="4"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            {{-- <x-admin::form.control-group.error control-name="blog_post_maximum_nested_comment"></x-admin::form.control-group.error> --}}
+
+                        </x-admin::form.control-group>
+
+                    </div>
+
+                </div>
+
+                <!-- Default Blog SEO Setting Section -->
+                <div class="p-4 w-50 bg-white dark:bg-gray-900 rounded-[4px] box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        {{ __('Default Blog SEO Setting') }}
+                    </p>
+
+                    <div class="mt-8">
+                        
+                        {{-- Meta Title --}}
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label>
+                                {{ __('Meta Title') }}
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="blog_seo_meta_title"
+                                id="blog_seo_meta_title"
+                                :value="old('blog_seo_meta_title') ?? $settings['blog_seo_meta_title']"
+                                label="Meta Title"
+                                placeholder="Meta Title"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        {{-- Meta Keywords --}}
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label>
+                                {{ __('Meta Keywords') }}
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="blog_seo_meta_keywords"
+                                id="blog_seo_meta_keywords"
+                                :value="old('blog_seo_meta_keywords') ?? $settings['blog_seo_meta_keywords']"
+                                label="Meta Keywords"
+                                placeholder="Meta Keywords"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                        {{-- Meta Description --}}
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label>
+                                {{ __('Meta Description') }}
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="blog_seo_meta_description"
+                                id="blog_seo_meta_description"
+                                :value="old('blog_seo_meta_description') ?? $settings['blog_seo_meta_description']"
+                                label="Meta Description"
+                                placeholder="Meta Description"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                    </div>
+
+                </div>
+
+                <v-wc-custom-js></v-wc-custom-js>
+
+            </div>
+
+        </div>
+
+        {!! view_render_event('admin.blogs.setting.after') !!}
+
+    </x-admin::form>
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-wc-custom-js-template">
+        
+    </script>
+
+    <script type="module">
+        app.component('v-wc-custom-js', {
+            template: '#v-wc-custom-js-template',
+
+            data() {
+                return {
+                    
+                }
+            },
+
+            mounted() {
+                let self = this;
+                
+                document.getElementById('switch_blog_post_show_categories_with_count').addEventListener('change', function(e) {
+                    document.getElementById('blog_post_show_categories_with_count').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+                
+                document.getElementById('switch_blog_post_show_tags_with_count').addEventListener('change', function(e) {
+                    document.getElementById('blog_post_show_tags_with_count').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+                
+                document.getElementById('switch_blog_post_show_author_page').addEventListener('change', function(e) {
+                    document.getElementById('blog_post_show_author_page').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+                
+                document.getElementById('switch_blog_post_enable_comment').addEventListener('change', function(e) {
+                    document.getElementById('blog_post_enable_comment').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+                
+                document.getElementById('switch_blog_post_allow_guest_comment').addEventListener('change', function(e) {
+                    document.getElementById('blog_post_allow_guest_comment').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+                
+                document.getElementById('switch_blog_post_enable_comment_moderation').addEventListener('change', function(e) {
+                    document.getElementById('blog_post_enable_comment_moderation').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+
+            },
+        });
+    </script>
+@endPushOnce
+
+</x-admin::layouts>

+ 369 - 0
packages/Longyi/Blog/src/Resources/views/admin/tags/create.blade.php

@@ -0,0 +1,369 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('blog::app.tag.add-title')
+    </x-slot:title>
+
+    @php
+        $currentLocale = core()->getRequestedLocale();
+    @endphp
+    
+    <!-- Blog Edit Form -->
+    <x-admin::form
+        :action="route('admin.blog.tag.store')"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        {!! view_render_event('admin.blog.tags.create.before') !!}
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                @lang('blog::app.tag.add-title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <!-- Back Button -->
+                <a
+                    href="{{ route('admin.blog.tag.index') }}"
+                    class="transparent-button hover:bg-gray-200 dark:hover:bg-gray-800 dark:text-white"
+                >
+                    @lang('admin::app.catalog.categories.edit.back-btn')
+                </a>
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                >
+                    @lang('blog::app.tag.create-btn-title')
+                </button>
+            </div>
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <!-- Left Section -->
+            <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+
+                <!-- General -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded-[4px] box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('admin::app.catalog.categories.create.general')
+                    </p>
+
+                    <!-- Locales -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="locale"
+                        value="{{ core()->getRequestedLocaleCode() }}"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Name -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.tag.name')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="name"
+                            value="{{ old('name') }}"
+                            label="{{ trans('blog::app.tag.name') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="name"
+                                id="name"
+                                v-bind="field"
+                                :class="[errors['{{ 'name' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.tag.name') }}"
+                                v-slugify-target:slug="setValues"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="name"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Slug -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.tag.slug')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="slug"
+                            value="{{ old('slug') }}"
+                            label="{{ trans('blog::app.tag.slug') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="slug"
+                                id="slug"
+                                v-bind="field"
+                                :class="[errors['{{ 'slug' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.tag.slug') }}"
+                                v-slugify-target:slug
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="slug"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Description -->
+                    <v-description>
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.tag.description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="description"
+                                id="description"
+                                class="description"
+                                rules="required"
+                                :value="old('description')"
+                                :label="trans('blog::app.tag.description')"
+                                :tinymce="true"
+                                :prompt="core()->getConfigData('general.magic_ai.content_generation.category_description_prompt')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error
+                                control-name="description"
+                            >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+                    </v-description>
+
+
+                    <div class="flex gap-12">
+                        <!-- Add Logo -->
+                        <div class="flex flex-col gap-2 w-2/5 mt-5">
+                            <p class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.tag.image')
+                            </p>
+
+                            <x-admin::media.images name="image" rules="required"></x-admin::media.images>
+
+                            <x-admin::form.control-group.error control-name="image"></x-admin::form.control-group.error>
+
+                        </div>
+
+                    </div>
+                </div>
+
+                <!-- SEO Deatils -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded-[4px] box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.tag.search_engine_optimization')
+                    </p>
+
+                    <!-- SEO Title & Description Blade Componnet -->
+                    {{-- <x-admin::seo/> --}}
+                    <v-seo-helper-custom></v-seo-helper-custom>
+
+                    <div class="mt-8">
+                        <!-- Meta Title -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_title')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_title"
+                                id="meta_title"
+                                rules="required"
+                                :value="old('meta_title')"
+                                :label="trans('blog::app.blog.meta_title')"
+                                :placeholder="trans('blog::app.blog.meta_title')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_title"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Keywords -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_keywords')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_keywords"
+                                rules="required"
+                                :value="old('meta_keywords')"
+                                :label="trans('blog::app.blog.meta_keywords')"
+                                :placeholder="trans('blog::app.blog.meta_keywords')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_keywords"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Description -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="meta_description"
+                                id="meta_description"
+                                rules="required"
+                                :value="old('meta_description')"
+                                :label="trans('blog::app.blog.meta_description')"
+                                :placeholder="trans('blog::app.blog.meta_description')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_description"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+                    </div>
+                </div>
+
+            </div>
+
+            <!-- Right Section -->
+            <div class="flex flex-col gap-2 w-[360px] max-w-full">
+                <!-- Settings -->
+
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-600 dark:text-gray-300 text-base font-semibold">
+                            @lang('admin::app.catalog.categories.create.settings')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.tag.status')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="status"
+                                class="cursor-pointer"
+                                value="1"
+                                :label="trans('blog::app.tag.status')"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                {!! view_render_event('admin.blog.tags.create.after') !!}
+
+            </div>
+        </div>
+
+    </x-admin::form>
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-seo-helper-custom-template">
+        <div class="flex flex-col gap-[3px] mb-[30px]">
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                v-text="metaTitle"
+            >
+            </p>
+
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                style="display: none;"
+                v-text="metaSlug"
+            >
+            </p>
+
+            <!-- SEO Meta Title -->
+            <p 
+                class="text-[#135F29]"
+                v-text="'{{ URL::to('/') }}/blog/tag/' + (metaSlug ? metaSlug.toLowerCase().replace(/\s+/g, '-') : '')"
+            >
+            </p>
+
+            <!-- SEP Meta Description -->
+            <p 
+                class="text-gray-600 dark:text-gray-300"
+                v-text="metaDescription"
+            >
+            </p>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-seo-helper-custom', {
+            template: '#v-seo-helper-custom-template',
+
+            data() {
+                return {
+                    metaTitle: this.$parent.getValues()['meta_title'],
+
+                    metaDescription: this.$parent.getValues()['meta_description'],
+
+                    metaSlug: this.$parent.getValues()['slug'],
+                }
+            },
+
+            mounted() {
+                let self = this;
+
+                self.metaTitle = document.getElementById('meta_title').value;
+
+                self.metaDescription = document.getElementById('meta_description').value;
+
+                self.metaSlug = document.getElementById('slug').value;
+
+                document.getElementById('meta_title').addEventListener('input', function(e) {
+                    self.metaTitle = e.target.value;
+                });
+
+                document.getElementById('meta_description').addEventListener('input', function(e) {
+                    self.metaDescription = e.target.value;
+                });
+
+                document.getElementById('name').addEventListener('input', function(e) {
+                    setTimeout(function(){
+                        var slug = document.getElementById('slug').value;
+                        self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                    }, 1000);
+                });
+
+                document.getElementById('slug').addEventListener('input', function(e) {
+                    var slug = e.target.value;
+                    self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                });
+
+            },
+        });
+    </script>
+
+@endPushOnce
+
+</x-admin::layouts>

+ 411 - 0
packages/Longyi/Blog/src/Resources/views/admin/tags/edit.blade.php

@@ -0,0 +1,411 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('blog::app.tag.edit-title')
+    </x-slot:title>
+
+    @php
+        $currentLocale = core()->getRequestedLocale();
+    @endphp
+    
+    <!-- Blog Edit Form -->
+    <x-admin::form
+        :action="route('admin.blog.tag.update', $tag->id)"
+        method="POST"
+        enctype="multipart/form-data"
+    >
+
+        {!! view_render_event('admin.blog.tags.edit.before') !!}
+
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                @lang('blog::app.tag.edit-title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <!-- Back Button -->
+                <a
+                    href="{{ route('admin.blog.tag.index') }}"
+                    class="transparent-button hover:bg-gray-200 dark:hover:bg-gray-800 dark:text-white"
+                >
+                    @lang('admin::app.catalog.categories.edit.back-btn')
+                </a>
+
+                <!-- Save Button -->
+                <button
+                    type="submit"
+                    class="primary-button"
+                >
+                    @lang('blog::app.tag.edit-btn-title')
+                </button>
+            </div>
+        </div>
+
+        <!-- Full Pannel -->
+        <div class="flex gap-2.5 mt-3.5 max-xl:flex-wrap">
+
+            <!-- Left Section -->
+            <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+
+                <!-- General -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded-[4px] box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('admin::app.catalog.categories.create.general')
+                    </p>
+
+                    <!-- Locales -->
+                    <x-admin::form.control-group.control
+                        type="hidden"
+                        name="locale"
+                        value="{{ core()->getRequestedLocaleCode() }}"
+                    >
+                    </x-admin::form.control-group.control>
+
+                    <!-- Name -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.tag.name')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="name"
+                            value="{{ old('name') ?? $tag->name }}"
+                            label="{{ trans('blog::app.tag.name') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="name"
+                                id="name"
+                                v-bind="field"
+                                :class="[errors['{{ 'name' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.tag.name') }}"
+                                v-slugify-target:slug="setValues"
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="name"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Slug -->
+                    <x-admin::form.control-group class="mb-2.5">
+                        <x-admin::form.control-group.label class="required">
+                            @lang('blog::app.tag.slug')
+                        </x-admin::form.control-group.label>
+
+                        <v-field
+                            type="text"
+                            name="slug"
+                            value="{{ old('slug') ?? $tag->slug }}"
+                            label="{{ trans('blog::app.tag.slug') }}"
+                            rules="required"
+                            v-slot="{ field }"
+                        >
+                            <input
+                                type="text"
+                                name="slug"
+                                id="slug"
+                                v-bind="field"
+                                :class="[errors['{{ 'slug' }}'] ? 'border border-red-600 hover:border-red-600' : '']"
+                                class="flex w-full min-h-[39px] py-2 px-3 border rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400 dark:bg-gray-900 dark:border-gray-800"
+                                placeholder="{{ trans('blog::app.tag.slug') }}"
+                                v-slugify-target:slug
+                            >
+                        </v-field>
+
+                        <x-admin::form.control-group.error
+                            control-name="slug"
+                        >
+                        </x-admin::form.control-group.error>
+                    </x-admin::form.control-group>
+
+                    <!-- Description -->
+                    <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                        <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                            @lang('blog::app.blog.description-and-images')
+                        </p>
+                        <v-description>
+                            <x-admin::form.control-group class="mb-2.5">
+                                <x-admin::form.control-group.label class="required">
+                                    @lang('blog::app.tag.description')
+                                </x-admin::form.control-group.label>
+
+                                <x-admin::form.control-group.control
+                                    type="textarea"
+                                    name="description"
+                                    id="description"
+                                    class="description"
+                                    rules="required"
+                                    :value="old('description') ?? $tag->description"
+                                    :label="trans('blog::app.tag.description')"
+                                    :tinymce="true"
+                                    :prompt="core()->getConfigData('general.magic_ai.content_generation.category_description_prompt')"
+                                    >
+                                </x-admin::form.control-group.control>
+
+                                <x-admin::form.control-group.error
+                                control-name="description"
+                                >
+                            </x-admin::form.control-group.error>
+                        </x-admin::form.control-group>
+                    </v-description>
+
+                    <div class="flex gap-12">
+                        <!-- Add Logo -->
+                        <div class="flex flex-col gap-2 w-2/5 mt-5">
+                            <p class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.tag.image')
+                            </p>
+
+                            <x-admin::media.images 
+                            name="image" 
+                            :uploaded-images="$tag->image ? [['id' => 'image', 'url' => $tag->image_url]] : []"
+                            >
+
+                        </x-admin::media.images>
+
+                    </div>
+
+                </div>
+
+            </div>
+        </div>
+
+                <!-- SEO Deatils -->
+                <div class="p-4 bg-white dark:bg-gray-900 rounded-[4px] box-shadow">
+                    <p class="mb-4 text-base text-gray-800 dark:text-white font-semibold">
+                        @lang('blog::app.tag.search_engine_optimization')
+                    </p>
+
+                    <!-- SEO Title & Description Blade Componnet -->
+                    {{-- <x-admin::seo/> --}}
+                    <v-seo-helper-custom></v-seo-helper-custom>
+
+                    <div class="mt-8">
+                        <!-- Meta Title -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_title')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_title"
+                                id="meta_title"
+                                rules="required"
+                                :value="old('meta_title') ?? $tag->meta_title"
+                                :label="trans('blog::app.blog.meta_title')"
+                                :placeholder="trans('blog::app.blog.meta_title')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_title"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Keywords -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_keywords')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="meta_keywords"
+                                rules="required"
+                                :value="old('meta_keywords') ?? $tag->meta_keywords"
+                                :label="trans('blog::app.blog.meta_keywords')"
+                                :placeholder="trans('blog::app.blog.meta_keywords')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_keywords"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+
+                        <!-- Meta Description -->
+                        <x-admin::form.control-group class="mb-2.5">
+                            <x-admin::form.control-group.label class="required">
+                                @lang('blog::app.blog.meta_description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="textarea"
+                                name="meta_description"
+                                id="meta_description"
+                                rules="required"
+                                :value="old('meta_description') ?? $tag->meta_description"
+                                :label="trans('blog::app.blog.meta_description')"
+                                :placeholder="trans('blog::app.blog.meta_description')"
+                            >
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="meta_description"></x-admin::form.control-group.error>
+
+                        </x-admin::form.control-group>
+                    </div>
+                </div>
+
+            </div>
+
+            <!-- Right Section -->
+            <div class="flex flex-col gap-2 w-[360px] max-w-full">
+                <!-- Settings -->
+
+                <x-admin::accordion>
+                    <x-slot:header>
+                        <p class="p-2.5 text-gray-600 dark:text-gray-300 text-base font-semibold">
+                            @lang('admin::app.catalog.categories.create.settings')
+                        </p>
+                    </x-slot:header>
+
+                    <x-slot:content>
+
+                        <!-- Status -->
+                        <v-setting-custom></v-setting-custom>
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="text-gray-800 dark:text-white font-medium">
+                                @lang('blog::app.tag.status')
+                            </x-admin::form.control-group.label>
+
+                            @php $selectedValue_status = old('status') ?: $tag->status @endphp
+
+                            <x-admin::form.control-group.control
+                                type="switch"
+                                name="status_switch"
+                                name="status_switch"
+                                class="cursor-pointer"
+                                value="1"
+                                :label="trans('blog::app.tag.status')"
+                                :checked="(boolean) $selectedValue_status"
+                            >
+                            </x-admin::form.control-group.control>
+                        </x-admin::form.control-group>
+
+                    </x-slot:content>
+                </x-admin::accordion>
+
+                {!! view_render_event('admin.blog.tags.edit.before', ['tag' => $tag]) !!}
+
+            </div>
+        </div>
+
+    </x-admin::form>
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-setting-custom-template">
+        <input type="hidden" name="status" id="status" value="@php echo $tag->status @endphp">
+    </script>
+
+    <script type="module">
+        app.component('v-setting-custom', {
+            template: '#v-setting-custom-template',
+
+            data() {
+                return {
+                    
+                }
+            },
+
+            mounted() {
+                let self = this;
+
+                document.getElementById('status_switch').addEventListener('change', function(e) {
+                    document.getElementById('status').value = ( e.target.checked == true || e.target.checked == 'true' ) ? 1 : 0;
+                });
+
+            },
+        });
+    </script>
+
+@endPushOnce
+
+@pushOnce('scripts')
+    {{-- SEO Vue Component Template --}}
+    <script type="text/x-template" id="v-seo-helper-custom-template">
+        <div class="flex flex-col gap-[3px] mb-[30px]">
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                v-text="metaTitle"
+            >
+            </p>
+
+            <p 
+                class="text-[#161B9D] dark:text-white"
+                style="display: none;"
+                v-text="metaSlug"
+            >
+            </p>
+
+            <!-- SEO Meta Title -->
+            <p 
+                class="text-[#135F29]"
+                v-text="'{{ URL::to('/') }}/blog/tag/' + (metaSlug ? metaSlug.toLowerCase().replace(/\s+/g, '-') : '')"
+            >
+            </p>
+
+            <!-- SEP Meta Description -->
+            <p 
+                class="text-gray-600 dark:text-gray-300"
+                v-text="metaDescription"
+            >
+            </p>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-seo-helper-custom', {
+            template: '#v-seo-helper-custom-template',
+
+            data() {
+                return {
+                    metaTitle: this.$parent.getValues()['meta_title'],
+
+                    metaDescription: this.$parent.getValues()['meta_description'],
+
+                    metaSlug: this.$parent.getValues()['slug'],
+                }
+            },
+
+            mounted() {
+                let self = this;
+
+                self.metaTitle = document.getElementById('meta_title').value;
+
+                self.metaDescription = document.getElementById('meta_description').value;
+
+                self.metaSlug = document.getElementById('slug').value;
+
+                document.getElementById('meta_title').addEventListener('input', function(e) {
+                    self.metaTitle = e.target.value;
+                });
+
+                document.getElementById('meta_description').addEventListener('input', function(e) {
+                    self.metaDescription = e.target.value;
+                });
+
+                document.getElementById('name').addEventListener('input', function(e) {
+                    setTimeout(function(){
+                        var slug = document.getElementById('slug').value;
+                        self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                    }, 1000);
+                });
+
+                document.getElementById('slug').addEventListener('input', function(e) {
+                    var slug = e.target.value;
+                    self.metaSlug = ( slug != '' && slug != null && slug != undefined ) ? slug : '';
+                });
+
+            },
+        });
+    </script>
+
+@endPushOnce
+
+</x-admin::layouts>

+ 28 - 0
packages/Longyi/Blog/src/Resources/views/admin/tags/index.blade.php

@@ -0,0 +1,28 @@
+<x-admin::layouts>
+    <x-slot:title>
+        {{ __('Blog Tags') }}
+    </x-slot:title>
+
+    <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+        <p class="text-xl text-gray-800 dark:text-white font-bold">
+            {{ __('Blog Tags') }}
+        </p>
+
+        <div class="flex gap-x-2.5 items-center">
+            @if (bouncer()->hasPermission('blog.tag.create'))
+                <a href="{{ route('admin.blog.tag.create') }}">
+                    <div class="primary-button">
+                        {{ __('Create Tag') }}
+                    </div>
+                </a>
+            @endif
+        </div>        
+    </div>
+
+    {!! view_render_event('bagisto.admin.catalog.categories.list.before') !!}
+
+    <x-admin::datagrid src="{{ route('admin.blog.tag.index') }}"></x-admin::datagrid>
+
+    {!! view_render_event('bagisto.admin.catalog.categories.list.after') !!}
+
+</x-admin::layouts>

Разница между файлами не показана из-за своего большого размера
+ 1200 - 0
packages/Longyi/Blog/src/Resources/views/custom-css/custom-css.blade.php


+ 153 - 0
packages/Longyi/Blog/src/Resources/views/shop/author/index.blade.php

@@ -0,0 +1,153 @@
+@php
+    $channel = core()->getCurrentChannel();
+@endphp
+
+
+{{-- SEO Meta Content --}}
+@push ('meta')
+    <meta name="title" content="{{ $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) }}" />
+
+    <meta name="description" content="{{ $blog_seo_meta_keywords ?? ( $channel->home_seo['meta_description'] ?? '' ) }}" />
+
+    <meta name="keywords" content="{{ $blog_seo_meta_description ?? ( $channel->home_seo['meta_keywords'] ?? '' ) }}" />
+@endPush
+
+<x-shop::layouts>
+    {{-- Page Title --}}
+    <x-slot:title>
+        Posts by {{ $author_data->author }}
+    </x-slot>
+
+    @push ('styles')
+
+        @include ('blog::custom-css.custom-css')
+
+    @endpush
+
+    <div class="main">
+
+        <div>
+            <div class="row col-12 remove-padding-margin"><!---->
+                <div id="home-right-bar-container" class="col-12 no-padding content">
+                    <div class="container-right row no-margin col-12 no-padding">
+                        <div id="blog" class="container mt-5">
+                            <div class="full-content-wrapper">
+                                <div class="col-lg-12"><h1 class="mb-3 page-title">Posts by {{ $author_data->author }}</h1></div>
+                                <div class="flex flex-wrap grid-wrap">
+                                    
+                                    <div class="column-9">
+
+                                        @if( !empty($blogs) &&  count($blogs) > 0 )
+
+                                            <div class="flex flex-wrap blog-grid-list">
+
+                                                @foreach($blogs as $blog)
+                                                    <div class="blog-post-item">
+                                                        <div class="blog-post-box">
+                                                            <div class="card mb-5">
+                                                                <div class="blog-grid-img">
+                                                                    @if( isset($blog->m_src) && !empty($blog->m_src) && !is_null($blog->m_src) )
+                                                                        <picture>
+                                                                            <source media="(max-width: 767px)" srcset="{{ '/storage/' . $blog->m_src }}">
+                                                                            <img
+                                                                                src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                                alt="{{ $blog->name }}"
+                                                                                class="card-img-top">
+                                                                        </picture>
+                                                                    @else
+                                                                        <img
+                                                                            src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                            alt="{{ $blog->name }}"
+                                                                            class="card-img-top">
+                                                                    @endif
+                                                                </div>
+                                                                <div class="card-body">
+                                                                    <h2 class="card-title"><a href="{{route('shop.article.view',[$blog->category->slug . '/' . $blog->slug])}}">{{ $blog->name }}</a></h2>
+                                                                    <div class="post-meta">
+                                                                        <p>
+                                                                            {{\Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $blog->created_at)->format('M j, Y') }} by
+                                                                            @if( (int)$show_author_page == 1 )
+                                                                                <a href="{{route('shop.blog.author.index',[$blog->author_id])}}">{{ $blog->author }}</a>
+                                                                            @else
+                                                                                <a>{{ $blog->author }}</a>
+                                                                            @endif
+                                                                        </p>
+                                                                    </div>
+                                                                    
+                                                                    @if( !empty($blog->assign_categorys) && count($blog->assign_categorys) > 0 )
+                                                                        <div class="post-categories">
+                                                                            <p>
+                                                                                @foreach($blog->assign_categorys as $assign_category)
+                                                                                    <a href="{{route('shop.blog.category.index',[$assign_category->slug])}}" class="cat-link">{{$assign_category->name}}</a>
+                                                                                @endforeach
+                                                                            </p>
+                                                                        </div>
+                                                                    @endif
+
+                                                                    <div class="card-text text-justify">
+                                                                        {!! $blog->short_description !!}
+                                                                    </div>
+                                                                </div>
+                                                                <div class="card-footer">
+                                                                    <a href="{{route('shop.article.view',[$blog->category->slug . '/' . $blog->slug])}}" class="text-uppercase btn-text-link">Read more ></a>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                @endforeach
+
+                                                <div class="w-full col-lg-12 mt-5 mb-5">
+                                                    {!! $blogs->links() !!}
+                                                </div>
+
+                                            </div>
+
+                                        @else
+
+                                            <div class="post-not-available">No post published yet!!</div>
+
+                                        @endif
+
+                                    </div>
+
+                                    <div class=" column-3 blog-sidebar">
+                                        <div class="row">
+                                            <div class="col-lg-12 mb-4 categories">
+                                                <h3>Categories</h3>
+                                                <ul class="list-group">
+                                                    @foreach($categories as $category)
+                                                        <li><a href="{{route('shop.blog.category.index',[$category->slug])}}" class="list-group-item list-group-item-action">
+                                                                <span>{{ $category->name }}</span> 
+                                                                @if( (int)$show_categories_count == 1 )
+                                                                    <span class="badge badge-pill badge-primary">{{ $category->assign_blogs }}</span>
+                                                                @endif
+                                                        </a></li>
+                                                    @endforeach
+                                                </ul>
+
+                                                <div class="tags-part">
+                                                    <h3>Tags</h3> 
+                                                    <div class="tag-list">
+                                                        @foreach($tags as $tag)
+                                                            <a href="{{route('shop.blog.tag.index',[$tag->slug])}}" role="button" class="btn btn-primary btn-lg">{{ $tag->name }} 
+                                                                @if( (int)$show_tags_count == 1 )
+                                                                    <span class="badge badge-light">{{ $tag->count }}</span>
+                                                                @endif
+                                                            </a> 
+                                                        @endforeach
+                                                    </div>
+                                                </div>
+
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+    </div>
+</x-shop::layouts>

+ 167 - 0
packages/Longyi/Blog/src/Resources/views/shop/category/index.blade.php

@@ -0,0 +1,167 @@
+@php
+    $channel = core()->getCurrentChannel();
+@endphp
+
+
+{{-- SEO Meta Content --}}
+@push ('meta')
+    <meta name="title" content="{{ $category->meta_title ?? ( $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) ) }}" />
+
+    <meta name="description" content="{{ $category->meta_description ?? ( $blog_seo_meta_keywords ?? ( $channel->home_seo['meta_description'] ?? '' ) ) }}" />
+
+    <meta name="keywords" content="{{ $category->meta_keywords ?? ( $blog_seo_meta_description ?? ( $channel->home_seo['meta_keywords'] ?? '' ) ) }}" />
+@endPush
+
+<x-shop::layouts>
+    {{-- Page Title --}}
+    <x-slot:title>
+        {{ $category->meta_title ?? ( $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) ) }}
+    </x-slot>
+
+    @push ('styles')
+
+        @include ('blog::custom-css.custom-css')
+
+    @endpush
+
+    <div class="main">
+
+        <div>
+            <div class="row col-12 remove-padding-margin"><!---->
+                <div id="home-right-bar-container" class="col-12 no-padding content">
+                    <div class="container-right row no-margin col-12 no-padding">
+                        <div id="blog" class="container mt-5">
+                            <div class="full-content-wrapper">
+                                <!-- <div class="col-lg-12"><h1 class="mb-3 page-title">Our Blog</h1></div> -->
+                                <section class="blog-hero-wrapper">
+                                    <div class="blog-hero-image">
+                                        <h1 class="hero-main-title">{{ $category->name }}</h1>
+                                        <img
+                                        src="{{ '/storage/' . ( ( isset($category->image) && !empty($category->image) && !is_null($category->image) ) ? $category->image : 'placeholder-banner.jpg' ) }}"
+                                        alt="{{ $category->name }}"
+                                        class="card-img img-fluid img-thumbnail bg-fill">
+                                    </div>
+                                </section>
+                                <div class="flex flex-wrap grid-wrap">
+
+                                    <div class="column-12">
+                                        <div class="text-justify blog-post-content">
+                                            {!! $category->description !!}
+                                        </div>
+                                    </div>
+                                    
+                                    <div class="column-9">
+
+                                        @if( !empty($blogs) &&  count($blogs) > 0 )
+
+                                            <div class="flex flex-wrap blog-grid-list">
+
+                                                @foreach($blogs as $blog)
+                                                    <div class="blog-post-item">
+                                                        <div class="blog-post-box">
+                                                            <div class="card mb-5">
+                                                                <div class="blog-grid-img">
+                                                                    @if( isset($blog->m_src) && !empty($blog->m_src) && !is_null($blog->m_src) )
+                                                                        <picture>
+                                                                            <source media="(max-width: 767px)" srcset="{{ '/storage/' . $blog->m_src }}">
+                                                                            <img
+                                                                                src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                                alt="{{ $blog->name }}"
+                                                                                class="card-img-top">
+                                                                        </picture>
+                                                                    @else
+                                                                        <img
+                                                                            src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                            alt="{{ $blog->name }}"
+                                                                            class="card-img-top">
+                                                                    @endif
+                                                                </div>
+                                                                <div class="card-body">
+                                                                    <h2 class="card-title"><a href="{{route('shop.article.view',[$blog->category->slug . '/' . $blog->slug])}}">{{ $blog->name }}</a></h2>
+                                                                    <div class="post-meta">
+                                                                        <p>
+                                                                            {{\Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $blog->created_at)->format('M j, Y') }} by
+                                                                            @if( (int)$show_author_page == 1 )
+                                                                                <a href="{{route('shop.blog.author.index',[$blog->author_id])}}">{{ $blog->author }}</a>
+                                                                            @else
+                                                                                <a>{{ $blog->author }}</a>
+                                                                            @endif
+                                                                        </p>
+                                                                    </div>
+
+                                                                    @if( !empty($blog->assign_categorys) && count($blog->assign_categorys) > 0 )
+                                                                        <div class="post-categories">
+                                                                            <p>
+                                                                                @foreach($blog->assign_categorys as $assign_category)
+                                                                                    <a href="{{route('shop.blog.category.index',[$assign_category->slug])}}" class="cat-link">{{$assign_category->name}}</a>
+                                                                                @endforeach
+                                                                            </p>
+                                                                        </div>
+                                                                    @endif
+
+                                                                    <div class="card-text text-justify">
+                                                                        {!! $blog->short_description !!}
+                                                                    </div>
+                                                                </div>
+                                                                <div class="card-footer">
+                                                                    <a href="{{route('shop.article.view',[$blog->category->slug . '/' . $blog->slug])}}" class="text-uppercase btn-text-link">Read more ></a>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                @endforeach
+
+                                                <div class="w-full col-lg-12 mt-5 mb-5">
+                                                    {!! $blogs->links() !!}
+                                                </div>
+
+                                            </div>
+
+                                        @else
+
+                                            <div class="post-not-available">No post published yet!!</div>
+
+                                        @endif
+
+                                    </div>
+
+                                    <div class=" column-3 blog-sidebar">
+                                        <div class="row">
+                                            <div class="col-lg-12 mb-4 categories"><h3>Categories</h3>
+                                                <ul class="list-group">
+                                                    @foreach($categories as $category)
+                                                        <li><a href="{{route('shop.blog.category.index',[$category->slug])}}" class="list-group-item list-group-item-action">
+                                                                <span>{{ $category->name }}</span> 
+                                                                @if( (int)$show_categories_count == 1 )
+                                                                    <span class="badge badge-pill badge-primary">{{ $category->assign_blogs }}</span>
+                                                                @endif
+                                                        </a></li>
+                                                    @endforeach
+                                                </ul>
+
+                                                <div class="tags-part">
+                                                    <h3>Tags</h3> 
+                                                    <div class="tag-list">
+                                                        @foreach($tags as $tag)
+                                                            <a href="{{route('shop.blog.tag.index',[$tag->slug])}}" role="button" class="btn btn-primary btn-lg">{{ $tag->name }} 
+                                                                @if( (int)$show_tags_count == 1 )
+                                                                    <span class="badge badge-light">{{ $tag->count }}</span>
+                                                                @endif
+                                                            </a> 
+                                                        @endforeach
+                                                    </div>
+                                                </div>
+
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+    </div>
+</x-shop::layouts>

+ 63 - 0
packages/Longyi/Blog/src/Resources/views/shop/comment/list.blade.php

@@ -0,0 +1,63 @@
+@foreach( $comment_data as $comment )
+
+    <div id="comment-{{ $comment['id'] }}" class="media">
+        <img src="https://demo.thewebtech.co.in/vendor/blog/images/user.png" width="50" height="50" class="mr-3 rounded-circle" style="background-color: rgb(223, 223, 223);">
+        <div class="media-body">
+            <span class="comment_name">{{ $comment['name'] }}</span>
+            <span class="comment_created">{{ date_format(date_create($comment['created_at']), "F j, Y g:i a") }}</span>
+            <div class="comment d-inline-block w-100">{!! $comment['comment'] !!}</div>
+
+            @if( $nested_comment_index < $maximum_nested_comment )
+
+                @if( $guest_comment_flag )
+
+                    <button class="btn btn-primary btn-lg btn-reply">Reply</button> 
+                    <button class="btn btn-danger btn-lg btn-cancel-reply d-none" style="display: none;">Cancel Reply</button>
+                    <div class="row justify-content-center mt-3 d-none comment-form-holder" style="display: none;">
+                        <div class="col-md-12">
+                            <h3>Leave a comment</h3>
+                            <form method="POST" action="{{route('shop.blog.comment.store')}}" class="frmComment comment-form">
+                                @csrf
+                                <input type="hidden" name="parent_id" value="{{ $comment['id'] }}">
+                                <input type="hidden" name="post" value="{{ $blog->id }}">
+                                <div class="form-row">
+                                    <div class="form-group column-6">
+                                        <div class="input-group">
+                                            <div class="input-group-prepend">
+                                                <span class="input-group-text"><i class="fa fa-user"></i></span>
+                                            </div>
+                                            <input type="text" name="name" placeholder="Your Name" required="required" class="form-control" value="{{ ( isset($loggedIn_user_name) && !empty($loggedIn_user_name) && !is_null($loggedIn_user_name) ) ? $loggedIn_user_name : ''; }}">
+                                        </div>
+                                    </div>
+                                    <div class="form-group column-6">
+                                        <div class="input-group">
+                                            <div class="input-group-prepend">
+                                                <span class="input-group-text"><i class="fa fa-envelope"></i></span>
+                                            </div>
+                                            <input type="email" name="email" placeholder="Your Email" required="required" class="form-control" value="{{ ( isset($loggedIn_user_email) && !empty($loggedIn_user_email) && !is_null($loggedIn_user_email) ) ? $loggedIn_user_email : ''; }}">
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <textarea name="comment" placeholder="Your Comment" required="required" rows="5" class="form-control"></textarea>
+                                </div>
+                                <div class="form-group text-right">
+                                    <button type="submit" class="btn btn-primary btn-lg">Comment</button>
+                                </div>
+                            </form>
+                        </div>
+                    </div>
+
+                @endif
+
+                @if( array_key_exists('replay', $comment) && count($comment['replay']) > 0 )
+                    @php $nested_comment_index++;  @endphp
+                    @include ('blog::shop.comment.list', ['comment_data' => $comment['replay']])
+                @endif
+
+            @endif
+
+        </div>
+    </div>
+
+@endforeach

+ 13 - 0
packages/Longyi/Blog/src/Resources/views/shop/default/index.blade.php

@@ -0,0 +1,13 @@
+@extends('shop::layouts.master')
+
+@section('page_title')
+    Package Article
+@stop
+
+@section('content-wrapper')
+
+    <div class="main">
+        Package Article
+    </div>
+
+@stop

+ 167 - 0
packages/Longyi/Blog/src/Resources/views/shop/tag/index.blade.php

@@ -0,0 +1,167 @@
+@php
+    $channel = core()->getCurrentChannel();
+@endphp
+
+
+{{-- SEO Meta Content --}}
+@push ('meta')
+    <meta name="title" content="{{ $tag->meta_title ?? ( $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) ) }}" />
+
+    <meta name="description" content="{{ $tag->meta_description ?? ( $blog_seo_meta_keywords ?? ( $channel->home_seo['meta_description'] ?? '' ) ) }}" />
+
+    <meta name="keywords" content="{{ $tag->meta_keywords ?? ( $blog_seo_meta_description ?? ( $channel->home_seo['meta_keywords'] ?? '' ) ) }}" />
+@endPush
+
+<x-shop::layouts>
+    {{-- Page Title --}}
+    <x-slot:title>
+        {{ $tag->meta_title ?? ( $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) ) }}
+    </x-slot>
+
+    @push ('styles')
+
+        @include ('blog::custom-css.custom-css')
+
+    @endpush
+
+    <div class="main">
+
+        <div>
+            <div class="row col-12 remove-padding-margin"><!---->
+                <div id="home-right-bar-container" class="col-12 no-padding content">
+                    <div class="container-right row no-margin col-12 no-padding">
+                        <div id="blog" class="container mt-5">
+                            <div class="full-content-wrapper">
+                                <!-- <div class="col-lg-12"><h1 class="mb-3 page-title">Our Blog</h1></div> -->
+                                <section class="blog-hero-wrapper">
+                                    <div class="blog-hero-image">
+                                        <h1 class="hero-main-title">{{ $tag->name }}</h1>
+                                        <img
+                                        src="{{ '/storage/' . ( ( isset($tag->image) && !empty($tag->image) && !is_null($tag->image) ) ? $tag->image : 'placeholder-banner.jpg' ) }}"
+                                        alt=""
+                                        class="card-img img-fluid img-thumbnail bg-fill">
+                                    </div>
+                                </section>
+                                <div class="flex flex-wrap grid-wrap">
+
+                                    <div class="column-12">
+                                        <div class="text-justify blog-post-content">
+                                            {!! $tag->description !!}
+                                        </div>
+                                    </div>
+                                    
+                                    <div class="column-9">
+
+                                        @if( !empty($blogs) &&  count($blogs) > 0 )
+
+                                            <div class="flex flex-wrap blog-grid-list">
+
+                                                @foreach($blogs as $blog)
+                                                    <div class="blog-post-item">
+                                                        <div class="blog-post-box">
+                                                            <div class="card mb-5">
+                                                                <div class="blog-grid-img">
+                                                                    @if( isset($blog->m_src) && !empty($blog->m_src) && !is_null($blog->m_src) )
+                                                                        <picture>
+                                                                            <source media="(max-width: 767px)" srcset="{{ '/storage/' . $blog->m_src }}">
+                                                                            <img
+                                                                                src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                                alt="{{ $blog->name }}"
+                                                                                class="card-img-top">
+                                                                        </picture>
+                                                                    @else
+                                                                        <img
+                                                                            src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                            alt="{{ $blog->name }}"
+                                                                            class="card-img-top">
+                                                                    @endif
+                                                                </div>
+                                                                <div class="card-body">
+                                                                    <h2 class="card-title"><a href="{{route('shop.article.view',[$blog->category->slug . '/' . $blog->slug])}}">{{ $blog->name }}</a></h2>
+                                                                    <div class="post-meta">
+                                                                        <p>
+                                                                            {{\Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $blog->created_at)->format('M j, Y') }} by
+                                                                            @if( (int)$show_author_page == 1 )
+                                                                                <a href="{{route('shop.blog.author.index',[$blog->author_id])}}">{{ $blog->author }}</a>
+                                                                            @else
+                                                                                <a>{{ $blog->author }}</a>
+                                                                            @endif
+                                                                        </p>
+                                                                    </div>
+
+                                                                    @if( !empty($blog->assign_categorys) && count($blog->assign_categorys) > 0 )
+                                                                        <div class="post-categories">
+                                                                            <p>
+                                                                                @foreach($blog->assign_categorys as $assign_category)
+                                                                                    <a href="{{route('shop.blog.category.index',[$assign_category->slug])}}" class="cat-link">{{$assign_category->name}}</a>
+                                                                                @endforeach
+                                                                            </p>
+                                                                        </div>
+                                                                    @endif
+
+                                                                    <div class="card-text text-justify">
+                                                                        {!! $blog->short_description !!}
+                                                                    </div>
+                                                                </div>
+                                                                <div class="card-footer">
+                                                                    <a href="{{route('shop.article.view',[$blog->category->slug . '/' . $blog->slug])}}" class="text-uppercase btn-text-link">Read more ></a>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                @endforeach
+
+                                                <div class="w-full col-lg-12 mt-5 mb-5">
+                                                    {!! $blogs->links() !!}
+                                                </div>
+
+                                            </div>
+
+                                        @else
+
+                                            <div class="post-not-available">No post published yet!!</div>
+
+                                        @endif
+
+                                    </div>
+
+                                    <div class=" column-3 blog-sidebar">
+                                        <div class="row">
+                                            <div class="col-lg-12 mb-4 categories"><h3>Categories</h3>
+                                                <ul class="list-group">
+                                                    @foreach($categories as $category)
+                                                        <li><a href="{{route('shop.blog.category.index',[$category->slug])}}" class="list-group-item list-group-item-action">
+                                                                <span>{{ $category->name }}</span> 
+                                                                @if( (int)$show_categories_count == 1 )
+                                                                    <span class="badge badge-pill badge-primary">{{ $category->assign_blogs }}</span>
+                                                                @endif
+                                                        </a></li>
+                                                    @endforeach
+                                                </ul>
+
+                                                <div class="tags-part">
+                                                    <h3>Tags</h3> 
+                                                    <div class="tag-list">
+                                                        @foreach($tags as $tag)
+                                                            <a href="{{route('shop.blog.tag.index',[$tag->slug])}}" role="button" class="btn btn-primary btn-lg">{{ $tag->name }} 
+                                                                @if( (int)$show_tags_count == 1 )
+                                                                    <span class="badge badge-light">{{ $tag->count }}</span>
+                                                                @endif
+                                                            </a> 
+                                                        @endforeach
+                                                    </div>
+                                                </div>
+
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+    </div>
+</x-shop::layouts>

+ 153 - 0
packages/Longyi/Blog/src/Resources/views/shop/velocity/index.blade.php

@@ -0,0 +1,153 @@
+@php
+    $channel = core()->getCurrentChannel();
+@endphp
+
+
+{{-- SEO Meta Content --}}
+@push ('meta')
+    <meta name="title" content="{{ $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) }}" />
+
+    <meta name="description" content="{{ $blog_seo_meta_keywords ?? ( $channel->home_seo['meta_description'] ?? '' ) }}" />
+
+    <meta name="keywords" content="{{ $blog_seo_meta_description ?? ( $channel->home_seo['meta_keywords'] ?? '' ) }}" />
+@endPush
+
+<x-shop::layouts>
+    {{-- Page Title --}}
+    <x-slot:title>
+        {{ $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) }}
+    </x-slot>
+
+    @push ('styles')
+
+        @include ('blog::custom-css.custom-css')
+
+    @endpush
+
+    <div class="main">
+
+        <div>
+            <div class="row col-12 remove-padding-margin"><!---->
+                <div id="home-right-bar-container" class="col-12 no-padding content">
+                    <div class="container-right row no-margin col-12 no-padding">
+                        <div id="blog" class="container mt-5">
+                            <div class="full-content-wrapper">
+                                <div class="col-lg-12"><h1 class="mb-3 page-title">Our Blog</h1></div>
+                                <div class="flex flex-wrap grid-wrap">
+
+                                    <div class="column-9">
+
+                                        @if( !empty($blogs) &&  count($blogs) > 0 )
+
+                                            <div class="flex flex-wrap blog-grid-list">
+
+                                                @foreach($blogs as $blog)
+                                                    <div class="blog-post-item">
+                                                        <div class="blog-post-box">
+                                                            <div class="card mb-5">
+                                                                <div class="blog-grid-img">
+                                                                    @if( isset($blog->m_src) && !empty($blog->m_src) && !is_null($blog->m_src) )
+                                                                        <picture>
+                                                                            <source media="(max-width: 767px)" srcset="{{ '/storage/' . $blog->m_src }}">
+                                                                            <img
+                                                                                src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                                alt="{{ $blog->name }}"
+                                                                                class="card-img-top">
+                                                                        </picture>
+                                                                    @else
+                                                                        <img
+                                                                            src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                            alt="{{ $blog->name }}"
+                                                                            class="card-img-top">
+                                                                    @endif
+                                                                </div>
+                                                                <div class="card-body">
+                                                                    <h2 class="card-title"><a href="{{route('shop.article.view',[$blog->category->slug . '/' . $blog->slug])}}">{{ $blog->name }}</a></h2>
+                                                                    <div class="post-meta">
+                                                                        <p>
+                                                                            {{\Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $blog->created_at)->format('M j, Y') }} by
+                                                                            @if( (int)$show_author_page == 1 )
+                                                                                <a href="{{route('shop.blog.author.index',[$blog->author_id])}}">{{ $blog->author }}</a>
+                                                                            @else
+                                                                                <a>{{ $blog->author }}</a>
+                                                                            @endif
+                                                                        </p>
+                                                                    </div>
+                                                                    
+                                                                    @if( !empty($blog->assign_categorys) && count($blog->assign_categorys) > 0 )
+                                                                        <div class="post-categories">
+                                                                            <p>
+                                                                                @foreach($blog->assign_categorys as $assign_category)
+                                                                                    <a href="{{route('shop.blog.category.index',[$assign_category->slug])}}" class="cat-link">{{$assign_category->name}}</a>
+                                                                                @endforeach
+                                                                            </p>
+                                                                        </div>
+                                                                    @endif
+
+                                                                    <div class="card-text text-justify">
+                                                                        {!! $blog->short_description !!}
+                                                                    </div>
+                                                                </div>
+                                                                <div class="card-footer">
+                                                                    <a href="{{route('shop.article.view',[$blog->category->slug . '/' . $blog->slug])}}" class="text-uppercase btn-text-link">Read more ></a>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                @endforeach
+
+                                                <div class="w-full col-lg-12 mt-5 mb-5">
+                                                    {!! $blogs->links() !!}
+                                                </div>
+
+                                            </div>
+
+                                        @else
+
+                                            <div class="post-not-available">No post published yet!!</div>
+
+                                        @endif
+
+                                    </div>
+
+                                    <div class=" column-3 blog-sidebar">
+                                        <div class="row">
+                                            <div class="col-lg-12 mb-4 categories">
+                                                <h3>Categories</h3>
+                                                <ul class="list-group">
+                                                    @foreach($categories as $category)
+                                                        <li><a href="{{route('shop.blog.category.index',[$category->slug])}}" class="list-group-item list-group-item-action">
+                                                                <span>{{ $category->name }}</span> 
+                                                                @if( (int)$show_categories_count == 1 )
+                                                                    <span class="badge badge-pill badge-primary">{{ $category->assign_blogs }}</span>
+                                                                @endif
+                                                        </a></li>
+                                                    @endforeach
+                                                </ul>
+
+                                                <div class="tags-part">
+                                                    <h3>Tags</h3> 
+                                                    <div class="tag-list">
+                                                        @foreach($tags as $tag)
+                                                            <a href="{{route('shop.blog.tag.index',[$tag->slug])}}" role="button" class="btn btn-primary btn-lg">{{ $tag->name }} 
+                                                                @if( (int)$show_tags_count == 1 )
+                                                                    <span class="badge badge-light">{{ $tag->count }}</span>
+                                                                @endif
+                                                            </a> 
+                                                        @endforeach
+                                                    </div>
+                                                </div>
+
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+    </div>
+</x-shop::layouts>

+ 297 - 0
packages/Longyi/Blog/src/Resources/views/shop/velocity/view.blade.php

@@ -0,0 +1,297 @@
+@php
+    $channel = core()->getCurrentChannel();
+@endphp
+
+
+{{-- SEO Meta Content --}}
+@push ('meta')
+    <meta name="title" content="{{ $blog->meta_title ?? ( $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) ) }}" />
+
+    <meta name="description" content="{{ $blog->meta_description ?? ( $blog_seo_meta_keywords ?? ( $channel->home_seo['meta_description'] ?? '' ) ) }}" />
+
+    <meta name="keywords" content="{{ $blog->meta_keywords ?? ( $blog_seo_meta_description ?? ( $channel->home_seo['meta_keywords'] ?? '' ) ) }}" />
+@endPush
+
+<x-shop::layouts>
+    {{-- Page Title --}}
+    <x-slot:title>
+        {{ $blog->meta_title ?? ( $blog_seo_meta_title ?? ( $channel->home_seo['meta_title'] ?? '' ) ) }}
+    </x-slot>
+
+    @push ('styles')
+
+        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
+
+        @include ('blog::custom-css.custom-css')
+
+    @endpush
+
+    <div class="main">
+        <div>
+            <div class="row col-12 remove-padding-margin">
+                <div id="home-right-bar-container" class="col-12 no-padding content">
+                    <div class="container-right row no-margin col-12 no-padding">
+                        <section class="blog-hero-wrapper">
+                            <div class="blog-hero-image">
+                                <h1 class="hero-main-title">{{ $blog->name }}</h1>
+                                @if( isset($blog->m_src) && !empty($blog->m_src) && !is_null($blog->m_src) )
+                                    <picture>
+                                        <source media="(max-width: 767px)" srcset="{{ '/storage/' . $blog->m_src }}">
+                                        <img
+                                            src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-banner.jpg' ) }}"
+                                            alt="{{ $blog->name }}"
+                                            class="card-img img-fluid img-thumbnail bg-fill">
+                                    </picture>
+                                @else
+                                    <img
+                                        src="{{ '/storage/' . ( ( isset($blog->src) && !empty($blog->src) && !is_null($blog->src) ) ? $blog->src : 'placeholder-banner.jpg' ) }}"
+                                        alt="{{ $blog->name }}"
+                                        class="card-img img-fluid img-thumbnail bg-fill">
+                                @endif
+                            </div>
+                        </section>
+                        <div id="blog" class="container mt-5">
+
+                            <div class="full-content-wrapper">
+                                <div class="flex flex-wrap grid-wrap">
+                                    <div class="column-9">
+                                        <section class="blog-content">
+                                            <div class="text-justify mb-3 blog-post-content">
+                                                <h3 class="page-title">{{ $blog->name }}</h3>
+                                                <div class="post-tags mb-3">
+                                                    <strong>Tags:</strong>
+                                                    <div class="post-tag-lists">
+                                                        @if( !empty($blog_tags) && count($blog_tags) > 0 )
+                                                            @foreach( $blog_tags as $blog_tag )
+                                                                <a href="{{route('shop.blog.tag.index',[$blog_tag->slug])}}" class="cat-link">{{$blog_tag->name}}</a>
+                                                            @endforeach
+                                                        @endif
+                                                    </div>
+                                                </div>
+
+                                                {!! $blog->description !!}
+                                            </div>
+                                        </section>
+                                    </div>
+
+                                    <sidebar class="column-3 blog-sidebar">
+                                        <div class="row">
+                                            <div class="col-lg-12 mb-4 categories"><h3>Categories</h3>
+                                                <ul class="list-group">
+                                                    @foreach($categories as $category)
+                                                        <li><a href="{{route('shop.blog.category.index',[$category->slug])}}" class="list-group-item list-group-item-action">
+                                                                <span>{{ $category->name }}</span> 
+                                                                @if( (int)$show_categories_count == 1 )
+                                                                    <span class="badge badge-pill badge-primary">{{ $category->assign_blogs }}</span>
+                                                                @endif
+                                                        </a></li>
+                                                    @endforeach
+                                                </ul>
+
+                                                <div class="tags-part">
+                                                    <h3>Tags</h3> 
+                                                    <div class="tag-list">
+                                                        @foreach($tags as $tag)
+                                                            <a href="{{route('shop.blog.tag.index',[$tag->slug])}}" role="button" class="btn btn-primary btn-lg">{{ $tag->name }} 
+                                                                @if( (int)$show_tags_count == 1 )
+                                                                    <span class="badge badge-light">{{ $tag->count }}</span>
+                                                                @endif
+                                                            </a> 
+                                                        @endforeach
+                                                    </div>
+                                                </div>
+
+                                            </div>
+                                        </div>
+                                    </sidebar>
+                                </div>
+                            </div>
+
+                            <div id="comment-list" class="column-12 comment-part related-bolg-part">
+                                <div class="col-lg-12"><h1 class="mb-3 page-title">Related Blog</h1></div>
+                                <div class="flex flex-wrap blog-grid-list">
+
+                                    @foreach($related_blogs as $related_blog)
+                                        <div class="related-blog-post-item">
+                                            <div class="blog-post-box">
+                                                <div class="card mb-5">
+                                                    <div class="blog-grid-img">
+                                                        @if( isset($related_blog->m_src) && !empty($related_blog->m_src) && !is_null($related_blog->m_src) )
+                                                            <picture>
+                                                                <source media="(max-width: 767px)" srcset="{{ '/storage/' . $related_blog->m_src }}">
+                                                                <img
+                                                                    src="{{ '/storage/' . ( ( isset($related_blog->src) && !empty($related_blog->src) && !is_null($related_blog->src) ) ? $related_blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                    alt="{{ $related_blog->name }}"
+                                                                    class="card-img-top">
+                                                            </picture>
+                                                        @else
+                                                            <img
+                                                                src="{{ '/storage/' . ( ( isset($related_blog->src) && !empty($related_blog->src) && !is_null($related_blog->src) ) ? $related_blog->src : 'placeholder-thumb.jpg' ) }}"
+                                                                alt="{{ $related_blog->name }}"
+                                                                class="card-img-top">
+                                                        @endif
+                                                    </div>
+                                                    <div class="card-body">
+                                                        <h2 class="card-title"><a href="{{route('shop.article.view',[$related_blog->category->slug . '/' . $related_blog->slug])}}">{{ $related_blog->name }}</a></h2>
+                                                        <div class="post-meta">
+                                                            <p>
+                                                                {{\Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $related_blog->created_at)->format('M j, Y') }} by
+                                                                @if( (int)$show_author_page == 1 )
+                                                                    <a href="{{route('shop.blog.author.index',[$related_blog->author_id])}}">{{ $related_blog->author }}</a>
+                                                                @else
+                                                                    <a>{{ $blog->author }}</a>
+                                                                @endif
+                                                            </p>
+                                                        </div>
+
+                                                        @if( !empty($related_blog->assign_categorys) && count($related_blog->assign_categorys) > 0 )
+                                                            <div class="post-categories">
+                                                                <p>
+                                                                    @foreach($related_blog->assign_categorys as $assign_category)
+                                                                        <a href="{{route('shop.blog.category.index',[$assign_category->slug])}}" class="cat-link">{{$assign_category->name}}</a>
+                                                                    @endforeach
+                                                                </p>
+                                                            </div>
+                                                        @endif
+
+                                                        <div class="card-text text-justify">
+                                                            {!! $related_blog->short_description !!}
+                                                        </div>
+                                                    </div>
+                                                    <div class="card-footer">
+                                                        <a href="{{route('shop.article.view',[$related_blog->category->slug . '/' . $related_blog->slug])}}" class="text-uppercase btn-text-link">Read more ></a>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    @endforeach
+
+                                </div>
+                                
+                            </div>
+
+                            @if( (int)$enable_comment == 1 )
+
+                                <div id="comment-list" class="column-12 comment-part">
+                                    <div class="row flex flex-wrap grid-wrap">
+                                        <div class="column-12">
+                                            @if( (int)$blog->allow_comments == 1 )
+                                                <h2>Comments ({{ $total_comments_cnt }})</h2> 
+                                                <div class="row flex flex-wrap grid-wrap">
+
+                                                    @php
+
+                                                        $guest_comment_flag = false;
+                                                        if ( $loggedIn_user ) {
+                                                            $guest_comment_flag = true;
+                                                        } else {
+                                                            if ( (int)$allow_guest_comment == 1 ) {
+                                                                $guest_comment_flag = true;
+                                                            }
+                                                        }
+
+                                                    @endphp
+
+                                                    @if( $guest_comment_flag )
+
+                                                        <div class="column-12">
+                                                            <div class="row justify-content-center mt-3 comment-form-holder flex flex-wrap grid-wrap">
+                                                                <div class="column-12">
+                                                                    <h3>Leave a comment</h3> 
+                                                                    <form method="POST" action="{{route('shop.blog.comment.store')}}" class="frmComment comment-form">
+                                                                        @csrf
+                                                                        <input type="hidden" name="parent_id" value="0">
+                                                                        <input type="hidden" name="post" value="{{ $blog->id }}">
+                                                                        <div class="form-row">
+                                                                            <div class="form-group column-6">
+                                                                                <div class="input-group">
+                                                                                    <div class="input-group-prepend">
+                                                                                        <span class="input-group-text"><i class="fa fa-user"> </i></span>
+                                                                                    </div> 
+                                                                                    <input type="text" name="name" placeholder="Your Name" required="required" class="form-control" value="{{ ( isset($loggedIn_user_name) && !empty($loggedIn_user_name) && !is_null($loggedIn_user_name) ) ? $loggedIn_user_name : ''; }}">
+                                                                                </div>
+                                                                            </div>
+                                                                            <div class="form-group column-6">
+                                                                                <div class="input-group">
+                                                                                    <div class="input-group-prepend">
+                                                                                        <span class="input-group-text"><i class="fa fa-envelope"></i></span>
+                                                                                    </div>
+                                                                                    <input type="email" name="email" placeholder="Your Email" required="required" class="form-control" value="{{ ( isset($loggedIn_user_email) && !empty($loggedIn_user_email) && !is_null($loggedIn_user_email) ) ? $loggedIn_user_email : ''; }}">
+                                                                                </div>
+                                                                            </div>
+                                                                        </div>
+                                                                        <div class="form-group">
+                                                                            <textarea name="comment" placeholder="Your Comment" required="required" rows="5" class="form-control"></textarea>
+                                                                        </div>
+                                                                        <div class="form-group text-right">
+                                                                            <button type="submit" class="btn btn-primary btn-lg">Comment</button>
+                                                                        </div>
+                                                                    </form>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+
+                                                    @else
+
+                                                        <div class="column-12">
+                                                            <div class="comment-not-allow-guest">
+                                                                You must be logged in to comment. Clik <a href="{{ URL::to('/') }}/customer/login" target="_blank"> here</a> to login.
+                                                            </div>
+                                                        </div>
+
+                                                    @endif
+
+                                                    @if( !empty( $comments ) && count( $comments ) > 0 )
+
+                                                        @php $nested_comment_index = 0;  @endphp
+
+                                                        <div class="column-12">
+
+                                                            @include ('blog::shop.comment.list', ['comment_data' => $comments])
+
+                                                        </div>
+                                                        
+                                                    @endif
+
+                                                </div>
+                                            @else
+                                                <div class="comment-not-allow">Comments are turned off.</div>
+                                            @endif
+                                        </div>
+                                    </div>
+                                </div>
+
+                            @endif
+
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    @push ('scripts')
+
+        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
+
+        <script type="text/javascript">
+            
+            jQuery(document).on('click', '.btn-reply', function(event) {
+                var element = jQuery(this);
+                element.parent().find('.comment-form-holder:eq(0)').show();
+                element.hide();
+                element.next().show();
+            });
+
+            jQuery(document).on('click', '.btn-cancel-reply', function(event) {
+                var element = jQuery(this);
+                element.parent().find('.comment-form-holder:eq(0)').hide();
+                element.hide();
+                element.prev().show();
+            });
+
+        </script>
+
+    @endpush
+
+</x-shop::layouts>

+ 148 - 0
packages/Longyi/Blog/src/Routes/admin-routes.php

@@ -0,0 +1,148 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\Blog\Http\Controllers\Admin\BlogController;
+use Longyi\Blog\Http\Controllers\Admin\CategoryController;
+use Longyi\Blog\Http\Controllers\Admin\TagController;
+use Longyi\Blog\Http\Controllers\Admin\CommentController;
+use Longyi\Blog\Http\Controllers\Admin\SettingController;
+use Longyi\Blog\Http\Controllers\Admin\ImportController;
+
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => config('app.admin_url')], function () {
+
+    /**
+     * Admin blog routes
+     */
+    Route::get('blog', [BlogController::class, 'index'])->defaults('_config', [
+        'view' => 'blog::admin.blogs.index',
+    ])->name('admin.blog.index');
+
+    Route::get('blog/create', [BlogController::class, 'create'])->defaults('_config', [
+        'view' => 'blog::admin.blogs.create',
+    ])->name('admin.blog.create');
+
+    Route::get('/blog/edit/{id}', [BlogController::class, 'edit'])->defaults('_config', [
+        'view' => 'blog::admin.blogs.edit',
+    ])->name('admin.blog.edit');
+
+    Route::post('blog/store', [BlogController::class, 'store'])->defaults('_config', [
+        'redirect' => 'admin.blog.index',
+    ])->name('admin.blog.store');
+
+    Route::post('/blog/update/{id}', [BlogController::class, 'update'])->defaults('_config', [
+        'redirect' => 'admin.blog.index',
+    ])->name('admin.blog.update');
+
+    Route::post('/blog/delete/{id}', [BlogController::class, 'destroy'])->name('admin.blog.delete');
+
+    Route::post('blog/massdelete', [BlogController::class, 'massDestroy'])->name('admin.blog.massdelete');
+
+    /**
+     * Admin blog category routes
+     */
+    Route::get('blog/category', [CategoryController::class, 'index'])->defaults('_config', [
+        'view' => 'blog::admin.categories.index',
+    ])->name('admin.blog.category.index');
+
+    Route::get('blog/category/create', [CategoryController::class, 'create'])->defaults('_config', [
+        'view' => 'blog::admin.categories.create',
+    ])->name('admin.blog.category.create');
+
+    Route::get('/blog/category/edit/{id}', [CategoryController::class, 'edit'])->defaults('_config', [
+        'view' => 'blog::admin.categories.edit',
+    ])->name('admin.blog.category.edit');
+
+    Route::post('blog/category/store', [CategoryController::class, 'store'])->defaults('_config', [
+        'redirect' => 'admin.blog.category.index',
+    ])->name('admin.blog.category.store');
+
+    Route::post('/blog/category/update/{id}', [CategoryController::class, 'update'])->defaults('_config', [
+        'redirect' => 'admin.blog.category.index',
+    ])->name('admin.blog.category.update');
+
+    Route::post('/blog/category/delete/{id}', [CategoryController::class, 'destroy'])->name('admin.blog.category.delete');
+
+    Route::post('blog/category/massdelete', [CategoryController::class, 'massDestroy'])->name('admin.blog.category.massdelete');
+
+    /**
+     * Admin blog tag routes
+     */
+    Route::get('blog/tag', [TagController::class, 'index'])->defaults('_config', [
+        'view' => 'blog::admin.tags.index',
+    ])->name('admin.blog.tag.index');
+
+    Route::get('blog/tag/create', [TagController::class, 'create'])->defaults('_config', [
+        'view' => 'blog::admin.tags.create',
+    ])->name('admin.blog.tag.create');
+
+    Route::get('/blog/tag/edit/{id}', [TagController::class, 'edit'])->defaults('_config', [
+        'view' => 'blog::admin.tags.edit',
+    ])->name('admin.blog.tag.edit');
+
+    Route::post('blog/tag/store', [TagController::class, 'store'])->defaults('_config', [
+        'redirect' => 'admin.blog.tag.index',
+    ])->name('admin.blog.tag.store');
+
+    Route::post('/blog/tag/update/{id}', [TagController::class, 'update'])->defaults('_config', [
+        'redirect' => 'admin.blog.tag.index',
+    ])->name('admin.blog.tag.update');
+
+    Route::post('/blog/tag/delete/{id}', [TagController::class, 'destroy'])->name('admin.blog.tag.delete');
+
+    Route::post('blog/tag/massdelete', [TagController::class, 'massDestroy'])->name('admin.blog.tag.massdelete');
+
+    /**
+     * Admin blog comment routes
+     */
+    Route::get('blog/comment', [CommentController::class, 'index'])->defaults('_config', [
+        'view' => 'blog::admin.comments.index',
+    ])->name('admin.blog.comment.index');
+
+    Route::get('/blog/comment/edit/{id}', [CommentController::class, 'edit'])->defaults('_config', [
+        'view' => 'blog::admin.comments.edit',
+    ])->name('admin.blog.comment.edit');
+
+    Route::post('/blog/comment/update/{id}', [CommentController::class, 'update'])->defaults('_config', [
+        'redirect' => 'admin.blog.comment.index',
+    ])->name('admin.blog.comment.update');
+
+    Route::post('blog/comment/delete/{id}', [CommentController::class, 'destroy'])->name('admin.blog.comment.delete');
+
+    Route::post('blog/comment/massdelete', [CommentController::class, 'massDestroy'])->name('admin.blog.comment.massdelete');
+
+    /**
+     * Admin blog setting routes
+     */
+    Route::get('blog/setting', [SettingController::class, 'index'])->defaults('_config', [
+        'view' => 'blog::admin.setting.index',
+    ])->name('admin.blog.setting.index');
+
+    Route::post('blog/setting/store', [SettingController::class, 'store'])->defaults('_config', [
+        'redirect' => 'admin.blog.setting.index',
+    ])->name('admin.blog.setting.store');
+
+    /**
+     * Admin blog import routes
+     */
+    Route::get('blog/import/export', [ImportController::class, 'index'])->defaults('_config', [
+        'view' => 'blog::admin.import.index',
+    ])->name('admin.blog.import.export');
+
+    Route::post('blog/import/store', [ImportController::class, 'import'])->defaults('_config', [
+        'view' => 'blog::admin.import.index',
+    ])->name('admin.blog.import.store');
+
+    Route::get('blog/export/download', [ImportController::class, 'export'])->defaults('_config', [
+        'view' => 'blog::admin.import.index',
+    ])->name('admin.blog.export.download');
+
+});
+
+/**
+ * Admin blog API routes
+ */
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'api/v1/admin'], function () {
+
+    Route::get('blogs', [BlogController::class, 'gteBlogs'])->name('admin.blog.gteBlogs');
+
+});

+ 71 - 0
packages/Longyi/Blog/src/Routes/shop-routes.php

@@ -0,0 +1,71 @@
+<?php
+
+Route::group([
+    'prefix' => 'blog',
+    'middleware' => ['web', 'theme', 'locale', 'currency']
+], function () {
+
+    Route::get('/', 'Longyi\Blog\Http\Controllers\Shop\BlogController@index')->defaults('_config', [
+        'view' => 'blog::shop.velocity.index',
+    ])->name('shop.article.index');
+
+    Route::get('/author/{id}', 'Longyi\Blog\Http\Controllers\Shop\BlogController@authorPage')->defaults('_config', [
+        'view' => 'blog::shop.author.index',
+    ])->name('shop.blog.author.index');
+
+    Route::group(['prefix' => 'tag'], function () {
+
+        Route::get('/{slug}', 'Longyi\Blog\Http\Controllers\Shop\TagController@index')->defaults('_config', [
+            'view' => 'blog::shop.tag.index',
+        ])->name('shop.blog.tag.index');
+
+    });
+
+    Route::get('/{slug}', 'Longyi\Blog\Http\Controllers\Shop\CategoryController@index')->defaults('_config', [
+        'view' => 'blog::shop.category.index',
+    ])->name('shop.blog.category.index');
+
+    Route::get('/{slug}/{blog_slug?}', 'Longyi\Blog\Http\Controllers\Shop\BlogController@view')->defaults('_config', [
+        'view' => 'blog::shop.velocity.view',
+    ])->name('shop.article.view');
+
+
+});
+
+/**
+ * Shop blog API routes (公开只读接口).
+ */
+Route::group(['prefix' => 'api/v1/blog', 'middleware' => ['api']], function () {
+
+    $apiController = 'Longyi\Blog\Http\Controllers\Shop\API\Blogs\BlogController';
+
+    // 博客列表(支持 ?category=3,4&tag=1,2&s=&per_page=,category/tag 传 id,支持多个)
+    Route::get('/', $apiController.'@list')->name('shop.api.blog.list');
+
+    // 提交评论(静态前缀,需在动态路由前定义)
+    Route::post('/comment/store', 'Longyi\Blog\Http\Controllers\Shop\CommentController@store')->name('shop.api.blog.comment.store');
+
+    // 分类列表
+    Route::get('/category', $apiController.'@category_list')->name('shop.api.blog.category.list');
+
+    // 分类详情及其下博客
+    Route::get('/category/{slug}', $apiController.'@category_show')->name('shop.api.blog.category.show');
+
+    // 标签列表
+    Route::get('/tag', $apiController.'@tag_list')->name('shop.api.blog.tag.list');
+
+    // 标签详情及其下博客
+    Route::get('/tag/{slug}', $apiController.'@tag_show')->name('shop.api.blog.tag.show');
+
+    // 作者博客列表
+    Route::get('/author/{id}', $apiController.'@author_blogs')->name('shop.api.blog.author');
+
+    // 相关博客
+    Route::get('/{id}/related', $apiController.'@related')->name('shop.api.blog.related');
+
+    // 博客评论列表
+    Route::get('/{id}/comments', $apiController.'@comments')->name('shop.api.blog.comments');
+
+    // 博客详情(动态路由,需放在最后)
+    Route::get('/{id}', $apiController.'@show')->name('shop.api.blog.show');
+});

+ 126 - 0
packages/Longyi/Blog/src/Validations/BlogCategoryUniqueSlug.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace Longyi\Blog\Validations;
+
+use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Support\Facades\DB;
+use Webkul\Attribute\Repositories\AttributeRepository;
+use Webkul\Category\Models\CategoryTranslationProxy;
+use Webkul\Product\Repositories\ProductAttributeValueRepository;
+use Longyi\Blog\Models\Category;
+
+class BlogCategoryUniqueSlug implements Rule
+{
+    /**
+     * Reserved slugs.
+     *
+     * @var array
+     */
+    protected $reservedSlugs = [
+        'blog_categories',
+    ];
+
+    /**
+     * Is slug reserved.
+     *
+     * @var bool
+     */
+    protected $isSlugReserved = false;
+
+    /**
+     * Constructor.
+     *
+     * @param  string  $tableName
+     * @param  string  $id
+     */
+    public function __construct(
+        protected $tableName = null,
+        protected $id = null
+    ) {
+    }
+
+    /**
+     * Determine if the validation rule passes.
+     *
+     * @param  string  $attribute
+     * @param  mixed  $value
+     * @return bool
+     */
+    public function passes($attribute, $value)
+    {
+        if (in_array($value, $this->reservedSlugs)) {
+            return ! ($this->isSlugReserved = true);
+        }
+
+        return $this->isSlugUnique($value);
+    }
+
+    /**
+     * Get the validation error message.
+     *
+     * @return string
+     */
+    public function message()
+    {
+        if ($this->isSlugReserved) {
+            return trans('admin::app.validations.slug-reserved');
+        }
+
+        return trans('admin::app.validations.slug-being-used');
+    }
+
+    /**
+     * Checks slug is unique or not.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    protected function isSlugUnique($slug)
+    {
+        return ! $this->isSlugExistsInCategories($slug) && ! $this->isSlugExistsInProducts($slug);
+    }
+
+    /**
+     * Is slug is exists in categories.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    protected function isSlugExistsInCategories($slug)
+    {
+        if (
+            $this->tableName
+            && $this->id
+            && $this->tableName === 'blog_categories'
+        ) {
+            return Category::/*modelClass()::*/where('id', '<>', $this->id)
+                ->where('slug', $slug)
+                ->limit(1)
+                ->select(DB::raw(1))
+                ->exists();
+        }
+
+        return Category::/*modelClass()::*/where('slug', $slug)
+            ->limit(1)
+            ->select(DB::raw(1))
+            ->exists();
+    }
+
+    /**
+     * Is slug is exists in products.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    protected function isSlugExistsInProducts($slug)
+    {
+        $attribute = app(AttributeRepository::class)->findOneByField('code', 'url_key');
+
+        return ! app(ProductAttributeValueRepository::class)->isValueUnique(
+            $this->id,
+            $attribute->id,
+            $attribute->column_name,
+            request($attribute->code)
+        );
+    }
+}

+ 127 - 0
packages/Longyi/Blog/src/Validations/BlogTagUniqueSlug.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace Longyi\Blog\Validations;
+
+use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Support\Facades\DB;
+use Webkul\Attribute\Repositories\AttributeRepository;
+use Webkul\Category\Models\CategoryTranslationProxy;
+use Webkul\Product\Repositories\ProductAttributeValueRepository;
+use Longyi\Blog\Models\Tag;
+
+class BlogTagUniqueSlug implements Rule
+{
+    /**
+     * Reserved slugs.
+     *
+     * @var array
+     */
+    protected $reservedSlugs = [
+        'blog_tags',
+    ];
+
+    /**
+     * Is slug reserved.
+     *
+     * @var bool
+     */
+    protected $isSlugReserved = false;
+
+    /**
+     * Constructor.
+     *
+     * @param  string  $tableName
+     * @param  string  $id
+     */
+    public function __construct(
+        protected $tableName = null,
+        protected $id = null
+    ) {
+    }
+
+    /**
+     * Determine if the validation rule passes.
+     *
+     * @param  string  $attribute
+     * @param  mixed  $value
+     * @return bool
+     */
+    public function passes($attribute, $value)
+    {
+        if (in_array($value, $this->reservedSlugs)) {
+            return ! ($this->isSlugReserved = true);
+        }
+
+        return $this->isSlugUnique($value);
+    }
+
+    /**
+     * Get the validation error message.
+     *
+     * @return string
+     */
+    public function message()
+    {
+        if ($this->isSlugReserved) {
+            return trans('admin::app.validations.slug-reserved');
+        }
+
+        return trans('admin::app.validations.slug-being-used');
+    }
+
+    /**
+     * Checks slug is unique or not.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    protected function isSlugUnique($slug)
+    {
+        return ! $this->isSlugExistsInCategories($slug);
+        // return ! $this->isSlugExistsInCategories($slug) && ! $this->isSlugExistsInProducts($slug);
+    }
+
+    /**
+     * Is slug is exists in categories.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    protected function isSlugExistsInCategories($slug)
+    {
+        if (
+            $this->tableName
+            && $this->id
+            && $this->tableName === 'blog_tags'
+        ) {
+            return Tag::/*modelClass()::*/where('id', '<>', $this->id)
+                ->where('slug', $slug)
+                ->limit(1)
+                ->select(DB::raw(1))
+                ->exists();
+        }
+
+        return Tag::/*modelClass()::*/where('slug', $slug)
+            ->limit(1)
+            ->select(DB::raw(1))
+            ->exists();
+    }
+
+    /**
+     * Is slug is exists in products.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    /*protected function isSlugExistsInProducts($slug)
+    {
+        $attribute = app(AttributeRepository::class)->findOneByField('code', 'url_key');
+
+        return ! app(ProductAttributeValueRepository::class)->isValueUnique(
+            $this->id,
+            $attribute->id,
+            $attribute->column_name,
+            request($attribute->code)
+        );
+    }*/
+}

+ 127 - 0
packages/Longyi/Blog/src/Validations/BlogUniqueSlug.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace Longyi\Blog\Validations;
+
+use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Support\Facades\DB;
+use Webkul\Attribute\Repositories\AttributeRepository;
+use Webkul\Category\Models\CategoryTranslationProxy;
+use Webkul\Product\Repositories\ProductAttributeValueRepository;
+use Longyi\Blog\Models\Blog;
+
+class BlogUniqueSlug implements Rule
+{
+    /**
+     * Reserved slugs.
+     *
+     * @var array
+     */
+    protected $reservedSlugs = [
+        'blogs',
+    ];
+
+    /**
+     * Is slug reserved.
+     *
+     * @var bool
+     */
+    protected $isSlugReserved = false;
+
+    /**
+     * Constructor.
+     *
+     * @param  string  $tableName
+     * @param  string  $id
+     */
+    public function __construct(
+        protected $tableName = null,
+        protected $id = null
+    ) {
+    }
+
+    /**
+     * Determine if the validation rule passes.
+     *
+     * @param  string  $attribute
+     * @param  mixed  $value
+     * @return bool
+     */
+    public function passes($attribute, $value)
+    {
+        if (in_array($value, $this->reservedSlugs)) {
+            return ! ($this->isSlugReserved = true);
+        }
+
+        return $this->isSlugUnique($value);
+    }
+
+    /**
+     * Get the validation error message.
+     *
+     * @return string
+     */
+    public function message()
+    {
+        if ($this->isSlugReserved) {
+            return trans('admin::app.validations.slug-reserved');
+        }
+
+        return trans('admin::app.validations.slug-being-used');
+    }
+
+    /**
+     * Checks slug is unique or not.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    protected function isSlugUnique($slug)
+    {
+        return ! $this->isSlugExistsInCategories($slug);
+        // return ! $this->isSlugExistsInCategories($slug) && ! $this->isSlugExistsInProducts($slug);
+    }
+
+    /**
+     * Is slug is exists in categories.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    protected function isSlugExistsInCategories($slug)
+    {
+        if (
+            $this->tableName
+            && $this->id
+            && $this->tableName === 'blogs'
+        ) {
+            return Blog::/*modelClass()::*/where('id', '<>', $this->id)
+                ->where('slug', $slug)
+                ->limit(1)
+                ->select(DB::raw(1))
+                ->exists();
+        }
+
+        return Blog::/*modelClass()::*/where('slug', $slug)
+            ->limit(1)
+            ->select(DB::raw(1))
+            ->exists();
+    }
+
+    /**
+     * Is slug is exists in products.
+     *
+     * @param  string  $slug
+     * @return bool
+     */
+    /*protected function isSlugExistsInProducts($slug)
+    {
+        $attribute = app(AttributeRepository::class)->findOneByField('code', 'url_key');
+
+        return ! app(ProductAttributeValueRepository::class)->isValueUnique(
+            $this->id,
+            $attribute->id,
+            $attribute->column_name,
+            request($attribute->code)
+        );
+    }*/
+}

+ 24 - 0
packages/Longyi/Blog/webpack.mix.js

@@ -0,0 +1,24 @@
+const { mix } = require("laravel-mix");
+require("laravel-mix-merge-manifest");
+
+if (mix.inProduction()) {
+    var publicPath = 'publishable/assets';
+} else {
+    var publicPath = "../../../public/vendor/Webkul/Blog/assets";
+}
+
+mix.setPublicPath(publicPath).mergeManifest();
+mix.disableNotifications();
+
+
+mix.sass(__dirname + "/src/Resources/assets/sass/app.scss", "css/app.css")
+    .options({
+        processCssUrls: false
+    });
+
+mix.js(__dirname + "/src/Resources/assets/js/app.js", "js/app.js")
+    .copyDirectory( __dirname + '/src/Resources/assets/images', publicPath + '/images')
+
+if (mix.inProduction()) {
+    mix.version();
+}

+ 13 - 6
packages/Webkul/BagistoApi/src/Models/CustomerOrderItem.php

@@ -164,15 +164,20 @@ class CustomerOrderItem extends Model
     /**
      * Product main image (same structure as cart item base_image).
      *
-     * @return array<string, string>|null
+     * GraphQL exposes this as String, matching CartItemData::baseImage.
+     * Value is a JSON object: {"small_image_url":"...","medium_image_url":"...","large_image_url":"...","original_image_url":"..."}.
      */
-    #[ApiProperty(writable: false, description: 'Product base image URLs (small/medium/large/original)')]
-    public function getBaseImage(): ?array
+    #[ApiProperty(
+        writable: false,
+        description: 'Product base image URLs as JSON string (small/medium/large/original)',
+        schema: ['type' => 'string', 'nullable' => true],
+    )]
+    public function getBaseImage(): ?string
     {
         return $this->resolveBaseImage();
     }
 
-    public function getBaseImageAttribute(): ?array
+    public function getBaseImageAttribute(): ?string
     {
         return $this->resolveBaseImage();
     }
@@ -228,7 +233,7 @@ class CustomerOrderItem extends Model
         return $this->product;
     }
 
-    private function resolveBaseImage(): ?array
+    private function resolveBaseImage(): ?string
     {
         $product = $this->resolveImageProduct();
 
@@ -237,7 +242,9 @@ class CustomerOrderItem extends Model
         }
 
         try {
-            return ProductImage::getProductBaseImage($product);
+            $image = ProductImage::getProductBaseImage($product);
+
+            return is_array($image) ? json_encode($image) : null;
         } catch (\Throwable) {
             return null;
         }

+ 113 - 0
packages/Webkul/BagistoApi/tests/Feature/GraphQL/CustomerOrderTest.php

@@ -120,6 +120,81 @@ class CustomerOrderTest extends GraphQLTestCase
         expect($data['edges'])->not()->toBeEmpty();
     }
 
+    /**
+     * Test: Order item baseImage is a JSON string, not a GraphQL object
+     */
+    public function test_customer_orders_item_base_image_is_json_string(): void
+    {
+        $testData = $this->createTestData();
+
+        \Webkul\Product\Models\ProductImage::query()->create([
+            'product_id' => $testData['product']->id,
+            'path'       => 'product/15/test-image.webp',
+            'position'   => 1,
+        ]);
+
+        $query = <<<'GQL'
+            query getCustomerOrders {
+              customerOrders(first: 10) {
+                edges {
+                  node {
+                    _id
+                    items {
+                      edges {
+                        node {
+                          sku
+                          name
+                          baseImage
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+        GQL;
+
+        $response = $this->authenticatedGraphQL($testData['customer'], $query);
+
+        if ($response->json('errors.0.message') && str_contains((string) $response->json('errors.0.message'), 'edges')) {
+            $query = <<<'GQL'
+                query getCustomerOrders {
+                  customerOrders(first: 10) {
+                    edges {
+                      node {
+                        _id
+                        items {
+                          sku
+                          name
+                          baseImage
+                        }
+                      }
+                    }
+                  }
+                }
+            GQL;
+
+            $response = $this->authenticatedGraphQL($testData['customer'], $query);
+        }
+
+        $response->assertOk();
+        expect($response->json('errors'))->toBeNull();
+
+        $item = $response->json('data.customerOrders.edges.0.node.items.edges.0.node')
+            ?? $response->json('data.customerOrders.edges.0.node.items.0');
+        expect($item)->not()->toBeNull();
+        expect($item['baseImage'])->toBeString();
+
+        $decoded = json_decode($item['baseImage'], true);
+        expect($decoded)->toBeArray()
+            ->and($decoded)->toHaveKeys([
+                'small_image_url',
+                'medium_image_url',
+                'large_image_url',
+                'original_image_url',
+            ]);
+    }
+
     /**
      * Test: Unauthenticated request returns error
      */
@@ -554,4 +629,42 @@ class CustomerOrderTest extends GraphQLTestCase
             ->toContain('orderCurrencyCode')
             ->toContain('createdAt');
     }
+
+    /**
+     * Test: CustomerOrderItem.baseImage is a GraphQL String
+     */
+    public function test_customer_order_item_base_image_schema_is_string(): void
+    {
+        $query = <<<'GQL'
+            {
+              __type(name: "CustomerOrderItem") {
+                name
+                fields {
+                  name
+                  type {
+                    kind
+                    name
+                    ofType {
+                      kind
+                      name
+                    }
+                  }
+                }
+              }
+            }
+        GQL;
+
+        $response = $this->graphQL($query);
+
+        $response->assertSuccessful();
+
+        $fields = $response->json('data.__type.fields');
+        expect($fields)->not()->toBeNull();
+
+        $baseImage = collect($fields)->firstWhere('name', 'baseImage');
+        expect($baseImage)->not()->toBeNull();
+
+        $typeName = $baseImage['type']['name'] ?? $baseImage['type']['ofType']['name'] ?? null;
+        expect($typeName)->toBe('String');
+    }
 }

+ 53 - 0
packages/Webkul/BagistoApi/tests/Unit/Models/CustomerOrderItemBaseImageTest.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace Webkul\BagistoApi\Tests\Unit\Models;
+
+use Webkul\BagistoApi\Models\CustomerOrderItem;
+use Webkul\BagistoApi\Tests\BagistoApiTestCase;
+use Webkul\Product\Models\Product;
+use Webkul\Product\Models\ProductImage;
+
+class CustomerOrderItemBaseImageTest extends BagistoApiTestCase
+{
+    public function test_base_image_is_json_encoded_string(): void
+    {
+        $this->seedRequiredData();
+
+        $product = Product::factory()->create();
+
+        ProductImage::query()->create([
+            'product_id' => $product->id,
+            'path'       => 'product/15/test-image.webp',
+            'position'   => 1,
+        ]);
+
+        $item = new CustomerOrderItem;
+        $item->type = 'simple';
+        $item->setRelation('product', $product->load('images'));
+        $item->setRelation('children', collect());
+
+        $baseImage = $item->getBaseImage();
+
+        expect($baseImage)->toBeString();
+
+        $decoded = json_decode($baseImage, true);
+
+        expect($decoded)->toBeArray()
+            ->and($decoded)->toHaveKeys([
+                'small_image_url',
+                'medium_image_url',
+                'large_image_url',
+                'original_image_url',
+            ]);
+    }
+
+    public function test_base_image_is_null_without_product(): void
+    {
+        $item = new CustomerOrderItem;
+        $item->type = 'simple';
+        $item->setRelation('product', null);
+        $item->setRelation('children', collect());
+
+        expect($item->getBaseImage())->toBeNull();
+    }
+}