├── .editorconfig ├── .env.example ├── .env.githubaction ├── .gitattributes ├── .github └── workflows │ └── build_and_test.yml ├── .gitignore ├── .php_cs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── app ├── Console │ └── Commands │ │ ├── GenerateSitemap.php │ │ └── Utility │ │ └── SyncPermission.php ├── Http │ ├── Controllers │ │ ├── ArticleController.php │ │ ├── AuthController.php │ │ ├── CategoryController.php │ │ ├── CommentController.php │ │ ├── Controller.php │ │ ├── FileController.php │ │ ├── HomeController.php │ │ ├── KeywordController.php │ │ └── SubscriptionController.php │ └── View │ │ └── Composer │ │ ├── CategoriesComposer.php │ │ └── GlobalConfigComposer.php ├── Livewire │ ├── Backend │ │ ├── Article │ │ │ ├── Form.php │ │ │ ├── ImageForm.php │ │ │ ├── Index.php │ │ │ └── IndexRow.php │ │ ├── Category │ │ │ ├── Index.php │ │ │ └── IndexRow.php │ │ ├── Comment │ │ │ ├── Edit.php │ │ │ ├── Index.php │ │ │ ├── IndexRow.php │ │ │ └── Show.php │ │ ├── Config │ │ │ ├── ImageForm.php │ │ │ └── Index.php │ │ ├── Dashboard.php │ │ ├── Feedback │ │ │ └── Index.php │ │ ├── Keyword │ │ │ └── Index.php │ │ ├── Subscriber │ │ │ └── Index.php │ │ └── User │ │ │ ├── Form.php │ │ │ ├── Index.php │ │ │ ├── PasswordForm.php │ │ │ └── Profile.php │ └── Frontend │ │ ├── Article │ │ └── Comments.php │ │ ├── ContactForm.php │ │ └── Subscribe.php ├── Mail │ ├── CommentConfirmation.php │ ├── NotifyAdmin.php │ ├── NotifyCommentThread.php │ ├── NotifySubscriberForNewArticle.php │ └── SubscribeConfirmation.php ├── Models │ ├── Article.php │ ├── CanFormatDates.php │ ├── Category.php │ ├── Comment.php │ ├── Config.php │ ├── Feedback.php │ ├── Image.php │ ├── Keyword.php │ ├── Reader.php │ ├── Subscriber.php │ └── User.php ├── Providers │ ├── AppServiceProvider.php │ ├── TelescopeServiceProvider.php │ └── ViewServiceProvider.php └── Services │ └── Google │ └── GoogleDrive.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore ├── providers.php └── ssr │ ├── app.js │ └── ssr-manifest.json ├── composer.json ├── composer.lock ├── config ├── acl.php ├── app.php ├── auth.php ├── backup.php ├── blade-icons.php ├── blog.php ├── cache.php ├── database.php ├── fields.php ├── filesystems.php ├── livewire.php ├── logging.php ├── mail.php ├── permission.php ├── queue.php ├── sentry.php ├── services.php ├── session.php └── telescope.php ├── database ├── .gitignore ├── factories │ ├── ArticleFactory.php │ ├── CategoryFactory.php │ ├── CommentFactory.php │ ├── FeedbackFactory.php │ ├── KeywordFactory.php │ ├── SubscriberFactory.php │ └── UserFactory.php ├── migrations │ ├── .gitkeep │ ├── 2014_09_01_014904_create_images_table.php │ ├── 2014_10_11_100000_create_roles_table.php~ │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_000001_create_readers_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2016_10_01_014152_create_categories_table.php │ ├── 2016_10_01_014203_create_keywords_table.php │ ├── 2016_10_01_014543_create_articles_table.php │ ├── 2016_10_01_014930_create_comments_table.php │ ├── 2016_10_03_014544_create_article_image_table.php │ ├── 2016_10_03_190244_create_article_keyword_table.php │ ├── 2017_01_05_183023_create_jobs_table.php │ ├── 2017_02_25_150152_create_configs_table.php │ ├── 2017_02_27_161927_create_feedback_table.php │ ├── 2017_06_17_100129_add_is_comment_enabled_in_article_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2020_04_09_091100_create_cache_table.php │ ├── 2020_04_10_185833_create_permission_tables.php │ ├── 2021_07_08_075207_create_subscribers_table.php │ ├── 2021_07_08_125022_create_sessions_table.php │ ├── 2023_01_27_221841_add_slug_in_articles_table.php │ ├── 2023_07_15_104621_add_meta_in_articles_table.php │ ├── 2024_06_23_091013_create_password_reset_tokens_table.php │ ├── 2024_06_23_091256_create_job_batches_table.php │ ├── 2024_09_12_162739_create_telescope_entries_table.php │ └── 2024_09_12_230821_add_uuid_in_images_table.php └── seeders │ ├── .gitkeep │ ├── ArticlesTableSeeder.php │ ├── CategoriesTableSeeder.php │ ├── CommentsTableSeeder.php │ ├── ConfigsTableSeeder.php │ ├── DatabaseSeeder.php │ ├── KeywordsTableSeeder.php │ ├── ReadersTableSeeder.php │ ├── RolesAndPermissionsTableSeeder.php │ └── UsersTableSeeder.php ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.mjs ├── public ├── .htaccess ├── build │ ├── assets │ │ ├── app-Dl9rd-VE.css │ │ └── app-l0sNRNKZ.js │ └── manifest.json ├── favicon.ico ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ ├── glyphicons-halflings-regular.woff2 │ └── vendor │ │ ├── bootstrap │ │ └── dist │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── font-awesome │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── img │ ├── favicon.png │ ├── logo.png │ └── user.png ├── index.php ├── js │ └── shiki │ │ └── unpkg.com_shiki@0.14.3_dist_index.js ├── robots.txt ├── sitemap.xml └── web.config ├── resources ├── css │ └── app.css ├── js │ └── app.js └── views │ ├── backend │ ├── auth.blade.php │ ├── dashboard.blade.php │ └── users │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ ├── edit_password.blade.php │ │ └── show.blade.php │ ├── components │ ├── backend │ │ ├── comment.blade.php │ │ ├── comment │ │ │ └── publish-status.blade.php │ │ ├── form │ │ │ ├── button.blade.php │ │ │ ├── input.blade.php │ │ │ ├── label.blade.php │ │ │ ├── select.blade.php │ │ │ └── textarea.blade.php │ │ ├── navbar.blade.php │ │ ├── navbar │ │ │ └── navs.blade.php │ │ ├── reply.blade.php │ │ ├── table.blade.php │ │ └── table │ │ │ ├── td.blade.php │ │ │ └── th.blade.php │ ├── frontend │ │ ├── article.blade.php │ │ ├── article │ │ │ ├── comment.blade.php │ │ │ ├── comment │ │ │ │ └── form.blade.php │ │ │ ├── reply.blade.php │ │ │ ├── tag.blade.php │ │ │ └── tags.blade.php │ │ ├── footer.blade.php │ │ ├── google-analytics.blade.php │ │ ├── header.blade.php │ │ ├── meta │ │ │ ├── facebook.blade.php │ │ │ ├── google.blade.php │ │ │ └── twitter.blade.php │ │ ├── navbar.blade.php │ │ ├── seo-meta.blade.php │ │ └── social-links.blade.php │ ├── layouts │ │ ├── backend.blade.php │ │ └── frontend.blade.php │ ├── slideover.blade.php │ ├── status.blade.php │ ├── svg.blade.php │ └── toggle.blade.php │ ├── emails │ ├── comment_confirmation.blade.php │ ├── comment_thread_notification.blade.php │ ├── master.blade.php │ ├── notify_admin.blade.php │ ├── notify_new_article.blade.php │ ├── subscribe_confirmation.blade.php │ └── testMail.blade.php │ ├── errors │ └── 503.blade.php │ ├── frontend │ ├── articles │ │ ├── index.blade.php │ │ ├── search_result.blade.php │ │ └── show.blade.php │ ├── contact │ │ └── create.blade.php │ └── pages │ │ └── about.blade.php │ └── livewire │ ├── backend │ ├── article │ │ ├── form.blade.php │ │ ├── image-form.blade.php │ │ ├── index-row.blade.php │ │ └── index.blade.php │ ├── category │ │ ├── index-row.blade.php │ │ └── index.blade.php │ ├── comment │ │ ├── edit.blade.php │ │ ├── index-row.blade.php │ │ ├── index.blade.php │ │ └── show.blade.php │ ├── config │ │ ├── image-form.blade.php │ │ └── index.blade.php │ ├── dashboard.blade.php │ ├── feedback │ │ └── index.blade.php │ ├── keyword │ │ └── index.blade.php │ ├── subscriber │ │ └── index.blade.php │ └── user │ │ ├── form.blade.php │ │ ├── index.blade.php │ │ ├── password-form.blade.php │ │ └── profile.blade.php │ ├── frontend │ ├── article │ │ └── comments.blade.php │ ├── contact-form.blade.php │ └── subscribe.blade.php │ ├── placeholders │ ├── article.blade.php │ ├── cards.blade.php │ ├── default.blade.php │ └── list.blade.php │ └── user │ └── password-form.blade.php ├── routes ├── backend.php ├── console.php └── web.php ├── server.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.cjs ├── tests ├── Feature │ ├── Controllers │ │ ├── ArticleControllerTest.php │ │ ├── AuthControllerTest.php │ │ ├── CategoryControllerTest.php │ │ ├── CommentControllerTest.php │ │ ├── FeedbackControllerTest.php │ │ ├── HomeControllerTest.php │ │ ├── KeywordControllerTest.php │ │ └── SubscriptionControllerTest.php │ └── Livewire │ │ ├── Backend │ │ ├── Article │ │ │ ├── FormTest.php │ │ │ ├── IndexRowTest.php │ │ │ └── IndexTest.php │ │ ├── Category │ │ │ ├── IndexRowTest.php │ │ │ └── IndexTest.php │ │ ├── Comment │ │ │ ├── EditTest.php │ │ │ ├── IndexRowTest.php │ │ │ ├── IndexTest.php │ │ │ └── ShowTest.php │ │ ├── Config │ │ │ └── IndexTest.php │ │ ├── DashboardTest.php │ │ ├── Feedback │ │ │ └── IndexTest.php │ │ ├── Keyword │ │ │ └── IndexTest.php │ │ └── Subscriber │ │ │ └── IndexTest.php │ │ └── Frontend │ │ ├── Article │ │ └── CommentTest.php │ │ ├── ContactFormTest.php │ │ └── SubscribeTest.php ├── TestCase.php ├── Unit │ └── Models │ │ ├── ArticleTest.php │ │ ├── CategoryTest.php │ │ ├── CommentTest.php │ │ ├── ConfigTest.php │ │ ├── KeywordTest.php │ │ └── ReaderTest.php └── coverage │ └── badge-coverage.svg └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=LaraBlog 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | APP_MAINTENANCE_STORE=database 14 | 15 | BCRYPT_ROUNDS=12 16 | 17 | LOG_CHANNEL=stack 18 | LOG_STACK=single 19 | LOG_DEPRECATIONS_CHANNEL=null 20 | LOG_LEVEL=debug 21 | 22 | DB_CONNECTION=mysql 23 | DB_HOST=127.0.0.1 24 | DB_PORT=3306 25 | DB_DATABASE=larablog 26 | DB_USERNAME=root 27 | DB_PASSWORD= 28 | 29 | BROADCAST_CONNECTION=log 30 | FILESYSTEM_DISK=local 31 | QUEUE_CONNECTION=database 32 | 33 | CACHE_STORE=database 34 | CACHE_PREFIX= 35 | 36 | MEMCACHED_HOST=127.0.0.1 37 | 38 | REDIS_CLIENT=phpredis 39 | REDIS_HOST=127.0.0.1 40 | REDIS_PASSWORD=null 41 | REDIS_PORT=6379 42 | 43 | MAIL_MAILER=smtp 44 | MAIL_HOST=localhost 45 | MAIL_PORT=25 46 | MAIL_USERNAME=null 47 | MAIL_PASSWORD=null 48 | MAIL_ENCRYPTION=null 49 | MAIL_FROM_ADDRESS=null 50 | MAIL_FROM_NAME="${APP_NAME}" 51 | 52 | AWS_ACCESS_KEY_ID= 53 | AWS_SECRET_ACCESS_KEY= 54 | AWS_DEFAULT_REGION=us-east-1 55 | AWS_BUCKET= 56 | AWS_USE_PATH_STYLE_ENDPOINT=false 57 | 58 | VITE_APP_NAME="${APP_NAME}" 59 | 60 | GOOGLE_DRIVE_CLIENT_ID= 61 | GOOGLE_DRIVE_CLIENT_SECRET= 62 | GOOGLE_DRIVE_REFRESH_TOKEN= 63 | GOOGLE_DRIVE_FOLDER= 64 | 65 | GOOGLE_ANALYTICS_TRACKING_ID= 66 | 67 | SENTRY_LARAVEL_DSN= 68 | SENTRY_TRACES_SAMPLE_RATE=1.0 69 | 70 | -------------------------------------------------------------------------------- /.env.githubaction: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | APP_KEY= 3 | APP_DEBUG=true 4 | APP_LOG_LEVEL=debug 5 | APP_URL=http://localhost 6 | 7 | DB_CONNECTION=mysql 8 | DB_HOST=127.0.0.1 9 | DB_PORT=3306 10 | DB_DATABASE=lara_blog 11 | DB_USERNAME=root 12 | DB_PASSWORD=password 13 | 14 | BROADCAST_DRIVER=log 15 | CACHE_DRIVER=array 16 | SESSION_DRIVER=file 17 | QUEUE_DRIVER=database 18 | 19 | REDIS_HOST=127.0.0.1 20 | REDIS_PASSWORD=null 21 | REDIS_PORT=6379 22 | 23 | MAIL_DRIVER=smtp 24 | MAIL_HOST=localhost 25 | MAIL_PORT=25 26 | MAIL_USERNAME=null 27 | MAIL_PASSWORD=null 28 | MAIL_ENCRYPTION=null 29 | MAIL_FROM_ADDRESS=null 30 | MAIL_FROM_NAME="${APP_NAME}" 31 | 32 | PUSHER_APP_ID= 33 | PUSHER_KEY= 34 | PUSHER_SECRET= 35 | 36 | GOOGLE_ANALYTICS_TRACKING_ID= 37 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | *.php linguist-language=PHP 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .idea 3 | .php_cs.cache 4 | .vagrant 5 | Homestead.json 6 | Homestead.yaml 7 | /.phpunit.cache 8 | /node_modules 9 | npm-debug.log 10 | public/storage 11 | /public/hot 12 | public/mix-manifest.json 13 | storage/*.key 14 | vendor 15 | yarn-error.log 16 | _ide_helper.php 17 | *~ 18 | *.swp 19 | *.swo 20 | /clover.xml 21 | .phpunit.result.cache 22 | .composer.lock 23 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | notPath('bootstrap/cache') 4 | ->notPath('storage') 5 | ->notPath('vendor') 6 | ->in(__DIR__) 7 | ->name('*.php') 8 | ->name('_ide_helper') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return PhpCsFixer\Config::create() 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sortAlgorithm' => 'alpha'], 18 | //'no_unused_imports' => true, 19 | ]) 20 | ->setFinder($finder); -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | **Anyone is always welcome to contribute on the project. If you want to work with:** 2 | 1. Just create and issue(even if you want to fix the issue). 3 | 2. After fixing any issue or adding any new feature just send a pull request 4 | 3. I will be happy to add your code for the betterment of this project. 5 | Thanks. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Al- Imran Ahmed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # . 2 | 3 | Changes proposed in this pull request: 4 | - 5 | - 6 | - 7 | @alimranahmed 8 | -------------------------------------------------------------------------------- /app/Console/Commands/Utility/SyncPermission.php: -------------------------------------------------------------------------------- 1 | info('Syncing the roles and permission'); 17 | (new \RolesAndPermissionsTableSeeder())->run(); 18 | app(PermissionRegistrar::class)->forgetCachedPermissions(); 19 | $this->info('Synced roles an permission!'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | route('admin-dashboard'); 16 | } 17 | 18 | return view('backend.auth'); 19 | } 20 | 21 | public function login(Request $request): RedirectResponse 22 | { 23 | $request->validate([ 24 | 'email' => 'required|email', 25 | 'password' => 'required|min:4', 26 | ]); 27 | 28 | $credentials = $request->only('email', 'password'); 29 | 30 | $remember = $request->has('remember_me'); 31 | 32 | if (Auth::attempt($credentials, $remember)) { 33 | return redirect()->route('admin-dashboard'); 34 | } 35 | 36 | return back()->with('auth_error', 'Invalid credentials')->withInput(); 37 | } 38 | 39 | public function logout(): RedirectResponse 40 | { 41 | Auth::logout(); 42 | 43 | return redirect()->route('login-form'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Http/Controllers/CategoryController.php: -------------------------------------------------------------------------------- 1 | frontView = "frontend.$frontendDesign"; 16 | } 17 | 18 | public function getMessage(Exception $e, $msg = null) 19 | { 20 | if ($e instanceof ValidationException) { 21 | return $e->getMessage(); 22 | } 23 | 24 | if (env('APP_ENV') != 'production') { 25 | return $this->getLogMsg($e); 26 | } else { 27 | return is_null($msg) ? 'Oops, operation failed please try again' : $msg; 28 | } 29 | } 30 | 31 | public function getLogMsg(Exception $e): string 32 | { 33 | return $e->getLine().': '.$e->getFile().' '.$e->getMessage(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Controllers/FileController.php: -------------------------------------------------------------------------------- 1 | where('uuid', $imageUuidOrConfigName) 17 | ->value('src'); 18 | 19 | if ($src === null) { 20 | $src = Config::query() 21 | ->where('name', $imageUuidOrConfigName) 22 | ->value('value'); 23 | } 24 | 25 | if ($src == null) { 26 | abort(404); 27 | } 28 | 29 | return response()->download(Storage::path($src)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | route('admin-dashboard'); 16 | } else { 17 | return (new ArticleController())->index($request); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Controllers/KeywordController.php: -------------------------------------------------------------------------------- 1 | validate(['token' => 'required']); 14 | 15 | $subscriber = Subscriber::query()->where('token', $request->token)->first(); 16 | 17 | if (is_null($subscriber)) { 18 | return redirect()->route('home')->with('error', 'Invalid request'); 19 | } 20 | 21 | $subscriber->update(['verified_at' => now()]); 22 | 23 | return redirect()->route('home')->with('success', 'Congratulation, we are connected now!'); 24 | } 25 | 26 | public function unsubscribe(Request $request): RedirectResponse 27 | { 28 | $request->validate(['token' => 'required']); 29 | 30 | $subscriber = Subscriber::where('token', $request->token)->first(); 31 | 32 | if (is_null($subscriber)) { 33 | return redirect()->route('home')->with('errorMsg', 'Invalid request'); 34 | } 35 | 36 | $subscriber->update(['unsubscribed_at' => now()]); 37 | 38 | return redirect()->route('home')->with('successMsg', 'You have unsubscribed'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Http/View/Composer/CategoriesComposer.php: -------------------------------------------------------------------------------- 1 | categories = Category::getNonEmptyOnly(); 16 | } 17 | 18 | public function compose(View $view): void 19 | { 20 | $view->with('navCategories', $this->categories); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/View/Composer/GlobalConfigComposer.php: -------------------------------------------------------------------------------- 1 | globalConfigs = Config::allFormatted(); 16 | } 17 | 18 | public function compose(View $view): void 19 | { 20 | $view->with('globalConfigs', $this->globalConfigs); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Article/Index.php: -------------------------------------------------------------------------------- 1 | '$refresh']; 29 | 30 | private function getArticles() 31 | { 32 | $articles = Article::notDeleted() 33 | ->with('category', 'keywords', 'user') 34 | ->latest(); 35 | 36 | if (Auth::user()->hasRole(['author'])) { 37 | $articles = $articles->where('user_id', Auth::user()->id); 38 | } 39 | 40 | if ($this->category) { 41 | $articles = $articles->where('category_id', $this->category); 42 | } 43 | 44 | if ($this->keyword) { 45 | $articles = $articles->whereHas('keywords', function (Builder $builder) { 46 | return $builder->where('name', $this->keyword); 47 | }); 48 | } 49 | 50 | if ($this->query) { 51 | $articles = $articles->search($this->query); 52 | } 53 | 54 | return $articles->paginate(config('blog.item_per_page')); 55 | } 56 | 57 | public function placeholder(): View 58 | { 59 | return view('livewire.placeholders.list'); 60 | } 61 | 62 | public function render(): View 63 | { 64 | $articles = $this->getArticles(); 65 | 66 | return view('livewire.backend.article.index', compact('articles')); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Article/IndexRow.php: -------------------------------------------------------------------------------- 1 | article = $article; 16 | } 17 | 18 | public function render(): View 19 | { 20 | return view('livewire.backend.article.index-row'); 21 | } 22 | 23 | public function togglePublish(): void 24 | { 25 | $this->article->update([ 26 | 'is_published' => ! $this->article->is_published, 27 | 'published_at' => now(), 28 | ]); 29 | 30 | $this->article->refresh(); 31 | } 32 | 33 | public function destroy(): void 34 | { 35 | $this->article->update(['is_deleted' => 1]); 36 | 37 | $this->dispatch('articleDeleted')->to(Index::class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Category/Index.php: -------------------------------------------------------------------------------- 1 | '$refresh']; 19 | 20 | public function startAdding(): void 21 | { 22 | $this->adding = true; 23 | } 24 | 25 | public function store(): void 26 | { 27 | $data = $this->validate(['category.name' => 'required', 'category.alias' => 'required']); 28 | 29 | Category::query()->create(Arr::get($data, 'category')); 30 | 31 | $this->adding = false; 32 | } 33 | 34 | protected function getCategories(): LengthAwarePaginator 35 | { 36 | return Category::query() 37 | ->with(['articles' => fn (HasMany $articles) => $articles->notDeleted()]) 38 | ->orderBy('name') 39 | ->paginate(); 40 | } 41 | 42 | public function placeholder(): View 43 | { 44 | return view('livewire.placeholders.list'); 45 | } 46 | 47 | public function render(): View 48 | { 49 | $categories = $this->getCategories(); 50 | 51 | return view('livewire.backend.category.index', compact('categories')); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Category/IndexRow.php: -------------------------------------------------------------------------------- 1 | category = $category; 21 | } 22 | 23 | public function render(): View 24 | { 25 | return view('livewire.backend.category.index-row'); 26 | } 27 | 28 | public function toggleActive(): void 29 | { 30 | $this->category->update(['is_active' => ! $this->category->is_active]); 31 | $this->category->refresh(); 32 | } 33 | 34 | public function destroy(Category $category): void 35 | { 36 | $category->delete(); 37 | $this->dispatch('categoryDeleted')->to(Index::class); 38 | } 39 | 40 | public function startEditing(): void 41 | { 42 | $this->editing = true; 43 | $this->categoryData = $this->category->toArray(); 44 | } 45 | 46 | public function update(): void 47 | { 48 | $data = $this->validate(['categoryData.name' => 'required', 'categoryData.alias' => 'required']); 49 | 50 | $data = Arr::get($data, 'categoryData'); 51 | 52 | $this->category->update($data); 53 | 54 | $this->editing = false; 55 | 56 | $this->category->refresh(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Comment/Edit.php: -------------------------------------------------------------------------------- 1 | 'string|required', 15 | 'is_published' => 'boolean', 16 | ]; 17 | 18 | public Comment $comment; 19 | 20 | public ?string $content; 21 | 22 | public ?bool $is_published = false; 23 | 24 | public function mount(Comment $comment): void 25 | { 26 | $this->comment = $comment; 27 | $this->content = $comment->content; 28 | $this->is_published = (bool) $this->is_published; 29 | } 30 | 31 | public function update(): RedirectResponse|Redirector 32 | { 33 | $this->validate(); 34 | 35 | $this->comment->update([ 36 | 'is_published' => $isPublished = $this->is_published, 37 | 'published_at' => $isPublished ? now() : null, 38 | 'content' => $this->content, 39 | 'original_content' => $this->comment->count_edit ? $this->comment->original_content : $this->comment->content, 40 | 'count_edit' => $this->comment->count_edit + 1, 41 | ]); 42 | 43 | return redirect()->to(route('backend.comment.show', $comment->parent_comment_id ?? $this->comment->id)); 44 | } 45 | 46 | public function render(): View 47 | { 48 | return view('livewire.backend.comment.edit'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Comment/Index.php: -------------------------------------------------------------------------------- 1 | '$refresh']; 24 | 25 | private function getComments(): LengthAwarePaginator 26 | { 27 | $commentQuery = Comment::with('article', 'user', 'replies') 28 | ->latest() 29 | ->noReplies(); 30 | 31 | if (auth()->user()->hasRole('author')) { 32 | $authorsArticleIDs = Article::query()->where('user_id', Auth::user()->id)->pluck('id'); 33 | 34 | $commentQuery->whereIn('article_id', $authorsArticleIDs); 35 | } 36 | 37 | if ($this->article) { 38 | $commentQuery->where('article_id', $this->article); 39 | } 40 | 41 | return $commentQuery->paginate(config('blog.item_per_page')); 42 | } 43 | 44 | public function placeholder(): View 45 | { 46 | return view('livewire.placeholders.list'); 47 | } 48 | 49 | public function render(): View 50 | { 51 | $comments = $this->getComments(); 52 | 53 | return view('livewire.backend.comment.index', compact('comments')); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Comment/IndexRow.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 17 | } 18 | 19 | public function render(): View 20 | { 21 | return view('livewire.backend.comment.index-row'); 22 | } 23 | 24 | public function togglePublish(): void 25 | { 26 | $this->comment->update([ 27 | 'is_published' => ! $this->comment->is_published, 28 | 'published_at' => now(), 29 | ]); 30 | 31 | $this->comment->refresh(); 32 | } 33 | 34 | public function destroy(Comment $comment): void 35 | { 36 | Article::query() 37 | ->where('id', $comment->article_id) 38 | ->decrement('comment_count'); 39 | 40 | $comment->delete(); 41 | 42 | $this->dispatch('commentDeleted')->to(Index::class); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Comment/Show.php: -------------------------------------------------------------------------------- 1 | ['required'], 18 | ]; 19 | 20 | public function mount(Comment $comment): void 21 | { 22 | $this->comment = $comment; 23 | } 24 | 25 | public function render(): View 26 | { 27 | return view('livewire.backend.comment.show'); 28 | } 29 | 30 | public function submitReply(): void 31 | { 32 | $data = $this->validate(); 33 | Comment::query()->create([ 34 | 'content' => Arr::get($data, 'reply.content'), 35 | 'article_id' => $this->comment->article_id, 36 | 'user_id' => auth()->id(), 37 | 'parent_comment_id' => $this->comment->id, 38 | 'is_published' => true, 39 | 'published_at' => now(), 40 | 'is_confirmed' => 1, 41 | ]); 42 | 43 | $this->reset('reply'); 44 | 45 | $this->comment->refresh(); 46 | } 47 | 48 | public function delete(Comment $comment): void 49 | { 50 | $comment->delete(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Config/Index.php: -------------------------------------------------------------------------------- 1 | '', 16 | 'editingConfig.value' => 'required', 17 | ]; 18 | 19 | public function render(): View 20 | { 21 | $configs = Config::query() 22 | ->whereNotIn('name', [Config::FAVICON, Config::USER_PHOTO]) 23 | ->get(); 24 | 25 | return view('livewire.backend.config.index', compact('configs')); 26 | } 27 | 28 | public function startEditing(Config $config): void 29 | { 30 | $this->editingConfig = $config->toArray(); 31 | } 32 | 33 | public function update(): void 34 | { 35 | $data = $this->validate(); 36 | Config::query()->findOrFail($this->editingConfig['id']) 37 | ->update(Arr::get($data, 'editingConfig')); 38 | 39 | $this->reset(['editingConfig']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Dashboard.php: -------------------------------------------------------------------------------- 1 | withCount('articles')->get()->sortByDesc('articles_count'); 16 | $latestComments = Comment::query()->with('user')->latest()->take(3)->get(); 17 | $latestFeedbacks = Feedback::query()->where('is_closed', 0)->take(3)->get(); 18 | 19 | return view( 20 | 'livewire.backend.dashboard', 21 | compact('categories', 'latestComments', 'latestFeedbacks') 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Feedback/Index.php: -------------------------------------------------------------------------------- 1 | update(['is_resolved' => ! $feedback->is_resolved]); 19 | } 20 | 21 | public function close(Feedback $feedback): void 22 | { 23 | $feedback->update(['is_closed' => 1]); 24 | } 25 | 26 | public function placeholder(): View 27 | { 28 | return view('livewire.placeholders.cards'); 29 | } 30 | 31 | public function render(): View 32 | { 33 | $feedbacks = Feedback::query() 34 | ->where('is_closed', 0) 35 | ->latest() 36 | ->paginate(15); 37 | 38 | return view('livewire.backend.feedback.index', compact('feedbacks')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Keyword/Index.php: -------------------------------------------------------------------------------- 1 | articles()->detach(); 17 | $keyword->delete(); 18 | } 19 | 20 | public function placeholder(): View 21 | { 22 | return view('livewire.placeholders.cards'); 23 | } 24 | 25 | public function render(): View 26 | { 27 | $keywords = Keyword::with('articles')->paginate(25); 28 | 29 | return view('livewire.backend.keyword.index', compact('keywords')); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Livewire/Backend/Subscriber/Index.php: -------------------------------------------------------------------------------- 1 | latest()->paginate(); 21 | 22 | return view('livewire.backend.subscriber.index', compact('subscribers')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Livewire/Backend/User/Form.php: -------------------------------------------------------------------------------- 1 | 'required', 17 | 'userData.username' => '', 18 | 'userData.email' => 'required|email', 19 | 'userData.role' => 'required', 20 | ]; 21 | 22 | public array $userData = []; 23 | 24 | public function mount(?User $user): void 25 | { 26 | $this->user = $user; 27 | $this->userData = $user->toArray(); 28 | $this->userData['role'] = $user->roles()->value('id'); 29 | } 30 | 31 | public function render(): View 32 | { 33 | $roles = Role::all(); 34 | 35 | return view('livewire.backend.user.form', compact('roles')); 36 | } 37 | 38 | public function submit(): void 39 | { 40 | $data = $this->validate(); 41 | 42 | $personalData = Arr::get($data, 'userData'); 43 | 44 | if ($this->user->id) { 45 | $this->user->update($personalData); 46 | $this->user->roles()->detach(); 47 | $this->user->assignRole($personalData['role']); 48 | } else { 49 | /** @var User $user */ 50 | $user = User::query()->create($personalData); 51 | $user->assignRole($personalData['role']); 52 | } 53 | 54 | $this->redirect(route('backend.user.index'), true); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Livewire/Backend/User/Index.php: -------------------------------------------------------------------------------- 1 | update(['is_active' => ! $user->is_active]); 20 | } 21 | 22 | public function delete(User $user): void 23 | { 24 | if (auth()->id() == $user->id) { 25 | abort(Response::HTTP_UNAUTHORIZED); 26 | } 27 | $user->delete(); 28 | } 29 | 30 | public function placeholder(): View 31 | { 32 | return view('livewire.placeholders.list'); 33 | } 34 | 35 | public function render(): View 36 | { 37 | $users = User::with('roles')->latest()->paginate(config('blog.item_per_page')); 38 | 39 | return view('livewire.backend.user.index', compact('users')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Livewire/Backend/User/PasswordForm.php: -------------------------------------------------------------------------------- 1 | 'required', 22 | 'new_password' => 'required', 23 | 'confirm_new_password' => 'required|same:new_password', 24 | ]; 25 | 26 | public function render(): View 27 | { 28 | return view('livewire.backend.user.password-form'); 29 | } 30 | 31 | /** 32 | * @throws ValidationException 33 | */ 34 | public function update(): void 35 | { 36 | $this->validate(); 37 | 38 | if (! Hash::check($this->old_password, Auth::user()->password)) { 39 | throw ValidationException::withMessages(['old_password' => 'Incorrect password']); 40 | } 41 | 42 | User::query() 43 | ->where('id', Auth::user()->id) 44 | ->update(['password' => Hash::make($this->new_password)]); 45 | 46 | $this->reset('old_password', 'new_password', 'confirm_new_password'); 47 | 48 | $this->dispatch('success', ['message' => 'Password updated successfully!']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Livewire/Backend/User/Profile.php: -------------------------------------------------------------------------------- 1 | ['required', 'max:255', "not_regex:/(http|ftp|mailto|www\.|\.com)/"], 27 | 'email' => 'email|required|max:255', 28 | 'content' => ['required', 'max:1500', "not_regex:/(http|ftp|mailto|www\.|\.com)/"], 29 | ]; 30 | 31 | public function submit(): void 32 | { 33 | $this->validate(); 34 | 35 | $feedback = Feedback::query()->create([ 36 | 'email' => $this->email, 37 | 'name' => $this->name, 38 | 'content' => $this->content, 39 | 'ip' => $_SERVER['REMOTE_ADDR'] ?? null, 40 | ]); 41 | 42 | Mail::to(Config::get('admin_email'))->queue(new NotifyAdmin($feedback, route('login-form'))); 43 | 44 | $this->isSubmitted = true; 45 | } 46 | 47 | public function render(): View 48 | { 49 | return view('livewire.frontend.contact-form'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Livewire/Frontend/Subscribe.php: -------------------------------------------------------------------------------- 1 | 'required|email|max:255', 22 | ]; 23 | 24 | public function subscribe(): void 25 | { 26 | $this->validate(); 27 | 28 | /** @var Subscriber $subscriber */ 29 | $subscriber = Subscriber::query() 30 | ->firstOrCreate( 31 | ['email' => $this->email], 32 | ['token' => $this->generateUniqueToken()] 33 | ); 34 | 35 | Mail::to($this->email)->queue(new SubscribeConfirmation($subscriber)); 36 | 37 | $this->isSubscribed = true; 38 | } 39 | 40 | private function generateUniqueToken(): string 41 | { 42 | do { 43 | $token = Str::random(); 44 | } while (Subscriber::query()->where('token', $token)->exists()); 45 | 46 | return $token; 47 | } 48 | 49 | public function render(): View 50 | { 51 | return view('livewire.frontend.subscribe'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Mail/CommentConfirmation.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 24 | } 25 | 26 | /** 27 | * Build the message. 28 | * 29 | * @return $this 30 | */ 31 | public function build() 32 | { 33 | return $this->from(Config::get('admin_email'), Config::get('site_name')) 34 | ->subject('Confirm Your Comment') 35 | ->view('emails.comment_confirmation'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Mail/NotifyAdmin.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 24 | $this->url = $url; 25 | } 26 | 27 | /** 28 | * Build the message. 29 | * 30 | * @return $this 31 | */ 32 | public function build() 33 | { 34 | return $this->from(Config::get('admin_email'), Config::get('site_name')) 35 | ->subject('User response in your Blog') 36 | ->view('emails.notify_admin'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Mail/NotifyCommentThread.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 23 | } 24 | 25 | /** 26 | * Build the message. 27 | * 28 | * @return $this 29 | */ 30 | public function build() 31 | { 32 | return $this->from(Config::get('admin_email'), Config::get('site_name')) 33 | ->subject('Response on the comment thread you are following') 34 | ->view('emails.comment_thread_notification'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Mail/NotifySubscriberForNewArticle.php: -------------------------------------------------------------------------------- 1 | article = $article; 24 | $this->user = $user; 25 | } 26 | 27 | /** 28 | * Build the message. 29 | * 30 | * @return $this 31 | */ 32 | public function build() 33 | { 34 | return $this->from(Config::get('admin_email'), Config::get('site_name')) 35 | ->subject('New article Published') 36 | ->view('emails.notify_new_article'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Mail/SubscribeConfirmation.php: -------------------------------------------------------------------------------- 1 | subscriber = $subscriber; 20 | } 21 | 22 | /** 23 | * Build the message. 24 | * 25 | * @return $this 26 | */ 27 | public function build() 28 | { 29 | return $this->from(Config::get('admin_email'), Config::get('site_name')) 30 | ->subject('Confirm Your Subscription') 31 | ->view('emails.subscribe_confirmation'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Models/CanFormatDates.php: -------------------------------------------------------------------------------- 1 | created_at)->diffForHumans(); 10 | } 11 | 12 | public function getCreatedDateTimeFormattedAttribute() 13 | { 14 | return optional($this->created_at)->format('M d, Y h:i A'); 15 | } 16 | 17 | public function getUpdatedAtHumanDiffAttribute() 18 | { 19 | return optional($this->updated_at)->diffForHumans(); 20 | } 21 | 22 | public function getPublishedAtHumanDiffAttribute() 23 | { 24 | return optional($this->published_at)->diffForHumans(); 25 | } 26 | 27 | public function getPublishedDateFormattedAttribute() 28 | { 29 | return optional($this->published_at)->format('M d, Y'); 30 | } 31 | 32 | public function getPublishedDateTimeFormattedAttribute() 33 | { 34 | return optional($this->published_at)->format('M d, Y h:i A'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Models/Category.php: -------------------------------------------------------------------------------- 1 | hasMany(Article::class); 26 | } 27 | 28 | public function parent(): BelongsTo 29 | { 30 | return $this->belongsTo(Category::class, 'parent_category_id'); 31 | } 32 | 33 | public function children(): BelongsTo 34 | { 35 | return $this->belongsTo(Category::class, 'parent_category_id'); 36 | } 37 | 38 | public static function getNonEmptyOnly(): Collection 39 | { 40 | return Category::query()->whereHas('articles')->active()->get(); 41 | } 42 | 43 | public function scopeActive($query) 44 | { 45 | return $query->where('is_active', 1); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Models/Comment.php: -------------------------------------------------------------------------------- 1 | 'datetime']; 23 | 24 | public function article(): BelongsTo 25 | { 26 | return $this->belongsTo(Article::class); 27 | } 28 | 29 | public function user(): BelongsTo 30 | { 31 | return $this->belongsTo(User::class); 32 | } 33 | 34 | public function replies(): HasMany 35 | { 36 | return $this->hasMany(Comment::class, 'parent_comment_id'); 37 | } 38 | 39 | public function parentComment(): BelongsTo 40 | { 41 | return $this->belongsTo(Comment::class, 'parent_comment_id'); 42 | } 43 | 44 | public function scopePublished(Builder $builder): Builder 45 | { 46 | return $builder->where('is_published', 1); 47 | } 48 | 49 | public function scopeNoReplies(Builder $builder): Builder 50 | { 51 | return $builder->where('parent_comment_id', null); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Models/Config.php: -------------------------------------------------------------------------------- 1 | where('name', $name)->value('value'); 24 | } 25 | 26 | public static function allFormatted($isActive = 1): stdClass 27 | { 28 | $configs = new stdClass(); 29 | 30 | $dbConfigs = self::query()->where('is_active', $isActive)->get(); 31 | 32 | $dbConfigs->each(fn (self $config) => $configs->{$config->name} = $config->value); 33 | 34 | return $configs; 35 | } 36 | 37 | public static function getPath(string $name): string 38 | { 39 | $defaultPath = match ($name) { 40 | self::FAVICON => asset('img/favicon.png'), 41 | self::USER_PHOTO => asset('img/user.png'), 42 | }; 43 | 44 | if ($defaultPath == null) { 45 | throw new InvalidArgumentException('Invalid config name: '.$name); 46 | } 47 | 48 | $path = Config::query()->where('name', $name)->value('value'); 49 | 50 | return $path ? route('file', [$name]) : asset($defaultPath); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Models/Feedback.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Article::class, 'article_keyword'); 24 | } 25 | 26 | public static function getArticleIDs(Collection $keywords): array 27 | { 28 | $articleKeywords = DB::table('article_keyword') 29 | ->select('article_id', 'keyword_id') 30 | ->whereIn('keyword_id', $keywords->pluck('id')->toArray()) 31 | ->get(); 32 | 33 | return $articleKeywords->pluck('article_id')->toArray(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Models/Reader.php: -------------------------------------------------------------------------------- 1 | where('is_verified', 1); 15 | } 16 | 17 | public function scopeSubscribed(Builder $builder): Builder 18 | { 19 | return $builder->where('notify', 1); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Models/Subscriber.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | protected function casts(): array 33 | { 34 | return [ 35 | 'email_verified_at' => 'datetime', 36 | 'password' => 'hashed', 37 | ]; 38 | } 39 | 40 | public function articles(): HasMany 41 | { 42 | return $this->hasMany(Article::class); 43 | } 44 | 45 | public function image(): BelongsTo 46 | { 47 | return $this->belongsTo(Image::class); 48 | } 49 | 50 | public function reader(): HasOne 51 | { 52 | return $this->hasOne(Reader::class); 53 | } 54 | 55 | public function isReader(): bool 56 | { 57 | return ! is_null($this->reader); 58 | } 59 | 60 | public function scopeActive(Builder $builder): Builder 61 | { 62 | return $builder->where('is_active', 1); 63 | } 64 | 65 | public static function getSubscribedUsers(): Collection 66 | { 67 | $subscribedReadersIds = Reader::query() 68 | ->subscribed() 69 | ->verified() 70 | ->pluck('user_id'); 71 | 72 | return self::query()->whereIn('id', $subscribedReadersIds)->get(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | user->can() and @can() 19 | Gate::before(function ($user, $ability) { 20 | return $user->hasRole('owner') ? true : null; 21 | }); 22 | 23 | Paginator::useTailwind(); 24 | 25 | GoogleDrive::loadStorageDriver(); 26 | } 27 | 28 | /** 29 | * Register any application services. 30 | */ 31 | public function register(): void {} 32 | } 33 | -------------------------------------------------------------------------------- /app/Providers/TelescopeServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->environment('local')) { 22 | return true; 23 | } 24 | 25 | return $entry->isReportableException() || 26 | $entry->isFailedJob() || 27 | $entry->isScheduledTask() || 28 | $entry->hasMonitoredTag(); 29 | }); 30 | } 31 | 32 | /** 33 | * Register the Telescope gate. 34 | * 35 | * This gate determines who can access Telescope in non-local environments. 36 | */ 37 | protected function gate(): void 38 | { 39 | Gate::define('viewTelescope', function ($user) { 40 | return $user->hasRole('owner'); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Providers/ViewServiceProvider.php: -------------------------------------------------------------------------------- 1 | setClientId($config['clientId']); 27 | $client->setClientSecret($config['clientSecret']); 28 | $client->refreshToken($config['refreshToken']); 29 | 30 | $service = new Drive($client); 31 | $adapter = new GoogleDriveAdapter($service, $config['folder'] ?? '/', $options); 32 | $driver = new Filesystem($adapter); 33 | 34 | return new FilesystemAdapter($driver, $adapter); 35 | }); 36 | } catch (Exception $e) { 37 | // your exception handling logic 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 11 | web: __DIR__.'/../routes/web.php', 12 | commands: __DIR__.'/../routes/console.php', 13 | health: '/up', 14 | then: function () { 15 | Route::middleware('web') 16 | ->prefix('admin') 17 | ->group(base_path('routes/backend.php')); 18 | } 19 | ) 20 | ->withMiddleware(function (Middleware $middleware) { 21 | $middleware->alias([ 22 | 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, 23 | 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, 24 | ]); 25 | }) 26 | ->withExceptions(function (Exceptions $exceptions) { 27 | Integration::handles($exceptions); 28 | })->create(); 29 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | ['owner', 'admin', 'author', 'reader'], 5 | 6 | 'permissions' => [ 7 | 'article-read' => ['admin'], 8 | 'article-create' => ['admin'], 9 | 'article-edit' => ['admin'], 10 | 'article-delete' => ['admin'], 11 | 'category-read' => ['admin'], 12 | 'category-create' => ['admin'], 13 | 'category-edit' => ['admin'], 14 | 'category-delete' => ['admin'], 15 | 'comment-read' => ['admin'], 16 | 'comment-create' => ['admin'], 17 | 'comment-edit' => ['admin'], 18 | 'comment-delete' => ['admin'], 19 | 'keyword-read' => ['admin'], 20 | 'keyword-create' => ['admin'], 21 | 'keyword-edit' => ['admin'], 22 | 'keyword-delete' => ['admin'], 23 | ], 24 | ]; 25 | -------------------------------------------------------------------------------- /config/blog.php: -------------------------------------------------------------------------------- 1 | 15, 6 | ]; 7 | -------------------------------------------------------------------------------- /config/fields.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'bn' => 'বাংলা', 6 | 'en' => 'English', 7 | ], 8 | ]; 9 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | 'google' => [ 39 | 'analytics_tracking_id' => env('GOOGLE_ANALYTICS_TRACKING_ID'), 40 | ], 41 | 42 | ]; 43 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /database/factories/ArticleFactory.php: -------------------------------------------------------------------------------- 1 | $heading = $this->faker->sentence, 23 | 'content' => implode(' ', $this->faker->paragraphs(15)), 24 | 'published_at' => now(), 25 | 'is_published' => 1, 26 | 'is_deleted' => 0, 27 | 'user_id' => null, 28 | 'language' => $language = $this->faker->randomElement(['ben', 'eng']), 29 | 'slug' => Str::slug($heading, '-', $language), 30 | 'category_id' => Category::factory()->create()->id, 31 | ]; 32 | } 33 | 34 | public function published(): ArticleFactory 35 | { 36 | return $this->state([ 37 | 'is_published' => 1, 38 | 'published_at' => now(), 39 | ]); 40 | } 41 | 42 | public function unpublished(): ArticleFactory 43 | { 44 | return $this->state([ 45 | 'is_published' => 0, 46 | 'published_at' => new \DateTime('+3 days'), 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /database/factories/CategoryFactory.php: -------------------------------------------------------------------------------- 1 | $name = $this->faker->word, 'alias' => $name]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/factories/CommentFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->paragraph, 24 | 'article_id' => null, 25 | 'is_published' => 1, 26 | 'published_at' => now(), 27 | 'is_confirmed' => 1, 28 | 'user_id' => null, 29 | 'parent_comment_id' => null, 30 | ]; 31 | } 32 | 33 | public function published(): CommentFactory 34 | { 35 | return $this->state([ 36 | 'is_published' => 1, 37 | 'published_at' => now(), 38 | ]); 39 | } 40 | 41 | public function unpublished(): CommentFactory 42 | { 43 | return $this->state([ 44 | 'is_published' => 0, 45 | 'published_at' => null, 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /database/factories/FeedbackFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class FeedbackFactory extends Factory 12 | { 13 | protected $model = Feedback::class; 14 | 15 | /** 16 | * Define the model's default state. 17 | * 18 | * @return array 19 | */ 20 | public function definition(): array 21 | { 22 | return [ 23 | 'name' => $this->faker->name(), 24 | 'email' => $this->faker->unique()->safeEmail(), 25 | 'content' => $this->faker->text(), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/factories/KeywordFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/factories/SubscriberFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->email, 25 | 'token' => Str::random(), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 'Mr', 24 | 'name' => 'Al Imran Ahmed', 25 | 'username' => 'imran', 26 | 'email' => $this->faker->email, 27 | 'password' => bcrypt('secret'), 28 | 'token' => null, 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /database/migrations/2014_09_01_014904_create_images_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->text('src'); 19 | $table->text('caption')->nullable(); 20 | $table->enum('src_type', ['internal', 'external'])->default('internal'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('images'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2014_10_11_100000_create_roles_table.php~: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->string('level')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('roles'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('title', 10)->nullable(); 19 | $table->string('name')->index()->nullable(); 20 | $table->string('username')->index()->nullable(); 21 | $table->string('email')->unique(); 22 | $table->string('password')->nullable(); 23 | $table->text('website')->nullable(); 24 | $table->integer('image_id')->unsigned()->nullable(); 25 | $table->foreign('image_id') 26 | ->references('id') 27 | ->on('images') 28 | ->onDelete('cascade'); 29 | $table->integer('is_active')->unsigned()->default(1); 30 | $table->dateTime('last_active')->nullable(); 31 | $table->string('last_ip', 200)->nullable(); 32 | $table->string('token')->nullable(); 33 | $table->rememberToken(); 34 | $table->timestamps(); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::drop('users'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000001_create_readers_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 13 | $table->integer('user_id')->unsigned()->nullable(); 14 | $table->foreign('user_id') 15 | ->references('id') 16 | ->on('users') 17 | ->onDelete('cascade'); 18 | $table->integer('is_verified')->default(0); 19 | $table->integer('notify')->default(0); 20 | $table->string('token')->nullable(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('readers'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token')->index(); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::drop('password_resets'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2016_10_01_014152_create_categories_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->string('alias'); 20 | $table->integer('parent_category_id')->unsigned()->nullable(); 21 | /*$table->foreign('parent_category_id') 22 | ->references('id') 23 | ->on('categories');*/ 24 | $table->integer('position')->unsigned()->nullable(); 25 | $table->integer('is_active')->unsigned()->default(1); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('categories'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2016_10_01_014203_create_keywords_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->integer('is_active')->unsigned()->default(1); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('keywords'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2016_10_01_014543_create_articles_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->text('heading'); 19 | $table->text('content'); 20 | $table->integer('user_id')->unsigned()->nullable(); 21 | $table->foreign('user_id') 22 | ->references('id') 23 | ->on('users'); 24 | $table->dateTime('published_at'); 25 | $table->integer('is_published')->unsigned()->default(1); 26 | $table->integer('is_deleted')->unsigned()->default(0); 27 | $table->integer('hit_count')->unsigned()->default(0); 28 | $table->integer('comment_count')->unsigned()->default(0); 29 | $table->integer('vote')->unsigned()->default(0); 30 | $table->integer('category_id')->unsigned()->nullable(); 31 | $table->foreign('category_id') 32 | ->references('id') 33 | ->on('categories'); 34 | $table->string('language')->default('en'); 35 | $table->timestamps(); 36 | }); 37 | } 38 | 39 | /** 40 | * Reverse the migrations. 41 | * 42 | * @return void 43 | */ 44 | public function down() 45 | { 46 | Schema::dropIfExists('articles'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /database/migrations/2016_10_01_014930_create_comments_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->text('content'); 19 | $table->integer('article_id')->unsigned(); 20 | $table->foreign('article_id') 21 | ->references('id') 22 | ->on('articles') 23 | ->onDelete('cascade'); 24 | $table->integer('user_id')->unsigned()->nullable(); 25 | $table->foreign('user_id') 26 | ->references('id') 27 | ->on('users') 28 | ->onDelete('cascade'); 29 | $table->integer('parent_comment_id')->unsigned()->nullable(); 30 | $table->foreign('parent_comment_id') 31 | ->references('id') 32 | ->on('comments') 33 | ->onDelete('cascade'); 34 | $table->integer('is_published')->unsigned()->default(1); 35 | $table->dateTime('published_at')->nullable(); 36 | $table->integer('count_edit')->unsigned()->default(0); 37 | $table->text('original_content')->nullable(); 38 | $table->integer('is_confirmed')->unsigned()->default(0); 39 | $table->string('token')->nullable(); 40 | $table->timestamps(); 41 | }); 42 | } 43 | 44 | /** 45 | * Reverse the migrations. 46 | * 47 | * @return void 48 | */ 49 | public function down() 50 | { 51 | Schema::dropIfExists('comments'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /database/migrations/2016_10_03_014544_create_article_image_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('image_id')->unsigned(); 19 | $table->foreign('image_id') 20 | ->references('id') 21 | ->on('images'); 22 | $table->integer('article_id')->unsigned(); 23 | $table->foreign('article_id') 24 | ->references('id') 25 | ->on('articles'); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('article_image'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2016_10_03_190244_create_article_keyword_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('article_id')->unsigned(); 19 | $table->foreign('article_id') 20 | ->references('id') 21 | ->on('articles'); 22 | $table->integer('keyword_id')->unsigned(); 23 | $table->foreign('keyword_id') 24 | ->references('id') 25 | ->on('keywords'); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('article_keyword'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2017_01_05_183023_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('queue'); 19 | $table->longText('payload'); 20 | $table->tinyInteger('attempts')->unsigned(); 21 | $table->unsignedInteger('reserved_at')->nullable(); 22 | $table->unsignedInteger('available_at'); 23 | $table->unsignedInteger('created_at'); 24 | $table->index(['queue', 'reserved_at']); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('jobs'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2017_02_25_150152_create_configs_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->text('value'); 20 | $table->integer('is_active')->unsigned()->default(1); 21 | $table->integer('is_mandatory')->unsigned()->default(1); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('configs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2017_02_27_161927_create_feedback_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name')->nullable(); 19 | $table->string('email'); 20 | $table->text('content'); 21 | $table->text('ip')->nullable(); 22 | $table->integer('is_resolved')->unsigned()->default(0); 23 | $table->integer('is_closed')->unsigned()->default(0); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('feedbacks'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2017_06_17_100129_add_is_comment_enabled_in_article_table.php: -------------------------------------------------------------------------------- 1 | integer('is_comment_enabled') 18 | ->default(1) 19 | ->unsigned(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('articles', function (Blueprint $table) { 31 | $table->dropColumn('is_comment_enabled'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 16 | $table->string('uuid')->unique(); 17 | $table->text('connection'); 18 | $table->text('queue'); 19 | $table->longText('payload'); 20 | $table->longText('exception'); 21 | $table->timestamp('failed_at')->useCurrent(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('failed_jobs'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2020_04_09_091100_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2021_07_08_075207_create_subscribers_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name')->nullable(); 19 | $table->string('email')->unique(); 20 | $table->dateTime('verified_at')->nullable(); 21 | $table->string('token')->unique(); 22 | $table->dateTime('unsubscribed_at')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('subscribers'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2021_07_08_125022_create_sessions_table.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 18 | $table->foreignId('user_id')->nullable()->index(); 19 | $table->string('ip_address', 45)->nullable(); 20 | $table->text('user_agent')->nullable(); 21 | $table->text('payload'); 22 | $table->integer('last_activity')->index(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('sessions'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2023_01_27_221841_add_slug_in_articles_table.php: -------------------------------------------------------------------------------- 1 | string('slug')->after('id')->unique(); 13 | }); 14 | } 15 | 16 | public function down(): void 17 | { 18 | Schema::table('articles', function (Blueprint $table) { 19 | $table->dropColumn('slug'); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /database/migrations/2023_07_15_104621_add_meta_in_articles_table.php: -------------------------------------------------------------------------------- 1 | json('meta')->after('language')->nullable(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('articles', function (Blueprint $table) { 25 | $table->dropColumn('meta'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_06_23_091013_create_password_reset_tokens_table.php: -------------------------------------------------------------------------------- 1 | string('email')->primary(); 16 | $table->string('token'); 17 | $table->timestamp('created_at')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('password_reset_tokens'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_06_23_091256_create_job_batches_table.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 16 | $table->string('name'); 17 | $table->integer('total_jobs'); 18 | $table->integer('pending_jobs'); 19 | $table->integer('failed_jobs'); 20 | $table->longText('failed_job_ids'); 21 | $table->mediumText('options')->nullable(); 22 | $table->integer('cancelled_at')->nullable(); 23 | $table->integer('created_at'); 24 | $table->integer('finished_at')->nullable(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('job_batches'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2024_09_12_230821_add_uuid_in_images_table.php: -------------------------------------------------------------------------------- 1 | uuid()->after('id'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('images', function (Blueprint $table) { 25 | $table->dropColumn('uuid'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/seeders/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /database/seeders/ArticlesTableSeeder.php: -------------------------------------------------------------------------------- 1 | environment() != 'production') { 21 | $faker = Factory::create(); 22 | foreach (range(0, 10) as $i) { 23 | Article::factory()->create([ 24 | 'category_id' => $faker->randomElement(Category::all()->pluck('id')->toArray()), 25 | 'user_id' => $faker->randomElement(User::all()->pluck('id')->toArray()), 26 | ]); 27 | } 28 | $articles = Article::all(); 29 | foreach ($articles as $article) { 30 | $article->keywords()->attach($faker->numberBetween(1, 5)); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/seeders/CategoriesTableSeeder.php: -------------------------------------------------------------------------------- 1 | 'Object Oriented Programming', 18 | 'alias' => 'oop', 19 | 'created_at' => new DateTime(), 20 | 'updated_at' => new DateTime(), 21 | ]; 22 | 23 | $categories[] = [ 24 | 'name' => 'Programming', 25 | 'alias' => 'programming', 26 | 'created_at' => new DateTime(), 27 | 'updated_at' => new DateTime(), 28 | ]; 29 | 30 | $categories[] = [ 31 | 'name' => 'PHP', 32 | 'alias' => 'php', 33 | 'created_at' => new DateTime(), 34 | 'updated_at' => new DateTime(), 35 | ]; 36 | 37 | $categories[] = [ 38 | 'name' => 'Java', 39 | 'alias' => 'java', 40 | 'created_at' => new DateTime(), 41 | 'updated_at' => new DateTime(), 42 | ]; 43 | 44 | $categories[] = [ 45 | 'name' => 'Python', 46 | 'alias' => 'python', 47 | 'created_at' => new DateTime(), 48 | 'updated_at' => new DateTime(), 49 | ]; 50 | 51 | $categories[] = [ 52 | 'name' => 'Web Frontend', 53 | 'alias' => 'web_frontend', 54 | 'created_at' => new DateTime(), 55 | 'updated_at' => new DateTime(), 56 | ]; 57 | 58 | $categories[] = [ 59 | 'name' => 'Miscellaneous', 60 | 'alias' => 'misc', 61 | 'created_at' => new DateTime(), 62 | 'updated_at' => new DateTime(), 63 | ]; 64 | 65 | Category::query()->insert($categories); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /database/seeders/CommentsTableSeeder.php: -------------------------------------------------------------------------------- 1 | environment() != 'production') { 23 | foreach (Article::all() as $article) { 24 | Comment::factory()->count(3)->create([ 25 | 'article_id' => $article->id, 26 | 'user_id' => $faker->randomElement(User::all()->pluck('id')->toArray()), 27 | ]); 28 | $article->update(['comment_count' => 3]); 29 | } 30 | } 31 | DB::statement('SET FOREIGN_KEY_CHECKS=1;'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/seeders/ConfigsTableSeeder.php: -------------------------------------------------------------------------------- 1 | insert([ 16 | ['name' => 'site_name', 'value' => 'Al- Imran Ahmed (-'], 17 | ['name' => 'site_title', 'value' => 'Al- Imran Ahmed (-'], 18 | ['name' => 'copyright_owner', 'value' => 'Al- Imran Ahmed (-'], 19 | ['name' => 'admin_email', 'value' => 'al.imran.cse@gmail.com'], 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(RolesAndPermissionsTableSeeder::class); 15 | $this->call(UsersTableSeeder::class); 16 | $this->call(ReadersTableSeeder::class); 17 | $this->call(CategoriesTableSeeder::class); 18 | $this->call(KeywordsTableSeeder::class); 19 | $this->call(ArticlesTableSeeder::class); 20 | $this->call(CommentsTableSeeder::class); 21 | $this->call(ConfigsTableSeeder::class); 22 | if (app()->environment('local')) { 23 | $this->command->info('All table seeded successfully!'); 24 | $this->command->info('username: owner@gmail.com | password: owner'); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/seeders/KeywordsTableSeeder.php: -------------------------------------------------------------------------------- 1 | count(5)->create(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /database/seeders/ReadersTableSeeder.php: -------------------------------------------------------------------------------- 1 | create( 18 | [ 19 | 'is_verified' => 1, 20 | 'notify' => 1, 21 | ] 22 | ); 23 | Reader::query()->create( 24 | [ 25 | 'is_verified' => 0, 26 | 'notify' => 0, 27 | ] 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build && vite build --ssr" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.2", 10 | "autoprefixer": "^10.4.13", 11 | "laravel-vite-plugin": "^1.0", 12 | "tailwindcss": "^3.2.4", 13 | "vite": "^5.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/build/assets/app-l0sNRNKZ.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources/css/app.css": { 3 | "file": "assets/app-Dl9rd-VE.css", 4 | "src": "resources/css/app.css", 5 | "isEntry": true 6 | }, 7 | "resources/js/app.js": { 8 | "file": "assets/app-l0sNRNKZ.js", 9 | "name": "app", 10 | "src": "resources/js/app.js", 11 | "isEntry": true 12 | } 13 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap/dist/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/vendor/bootstrap/dist/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap/dist/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/vendor/bootstrap/dist/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap/dist/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/vendor/bootstrap/dist/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap/dist/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/vendor/bootstrap/dist/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/vendor/font-awesome/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/vendor/font-awesome/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/vendor/font-awesome/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/fonts/vendor/font-awesome/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/img/favicon.png -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/img/logo.png -------------------------------------------------------------------------------- /public/img/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimranahmed/LaraBlog/2179d56d2a8f1651b32be9656b23b27ad2cff167/public/img/user.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @import "../../vendor/tempest/highlight/src/Themes/Css/github-dark-dimmed.css"; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | 8 | body { 9 | font-family: Whitney SSm A, Whitney SSm B, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 10 | @apply text-slate-800; 11 | } 12 | 13 | h1 { 14 | @apply font-semibold text-2xl leading-relaxed; 15 | } 16 | 17 | h3, h2 { 18 | @apply font-medium text-xl leading-relaxed; 19 | } 20 | 21 | h4, h5, h6 { 22 | @apply font-medium text-lg leading-relaxed; 23 | } 24 | 25 | strong { 26 | @apply text-indigo-900 font-medium; 27 | } 28 | 29 | li{ 30 | @apply my-2; 31 | } 32 | 33 | li::marker { 34 | @apply text-slate-400; 35 | } 36 | 37 | p code, li code { 38 | @apply bg-indigo-50 text-zinc-700 rounded-lg px-1 border whitespace-nowrap; 39 | } 40 | 41 | pre code { 42 | @apply border-0 p-0 leading-normal; 43 | } 44 | 45 | .article-content h1, 46 | .article-content h2, 47 | .article-content h3, 48 | .article-content h4, 49 | .article-content h5, 50 | .article-content h6 { 51 | @apply mt-7 mb-2 text-fuchsia-900 font-semibold; 52 | } 53 | 54 | .article-content ol { 55 | @apply pl-10 list-decimal list-inside; 56 | } 57 | 58 | .article-content ul { 59 | @apply pl-10 list-disc list-inside; 60 | } 61 | 62 | .article-content p { 63 | @apply mb-3; 64 | } 65 | 66 | .article-content blockquote { 67 | @apply p-4 my-3 italic border-l-4 border-slate-400 text-slate-500; 68 | } 69 | 70 | .article-content a { 71 | @apply text-blue-400; 72 | } 73 | 74 | /* 75 | Margin and rounding are personal preferences, 76 | overflow-x-auto is recommended. 77 | */ 78 | pre { 79 | @apply my-8 rounded-lg overflow-x-auto; 80 | } 81 | 82 | /* 83 | Add some vertical padding and expand the width 84 | to fill its container. 85 | */ 86 | pre[data-lang] { 87 | @apply p-5 block 88 | } 89 | 90 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/backend/auth.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

Welcome...

6 |
7 |
8 |
9 | {{csrf_field()}} 10 |
{{session('auth_error')}}
11 |
12 | 15 |
16 |
17 | 19 |
20 |
21 | 24 |
25 | Login 26 | 32 |
33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /resources/views/backend/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/backend/users/create.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/backend/users/edit.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/backend/users/edit_password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/components/backend/comment.blade.php: -------------------------------------------------------------------------------- 1 | @props(['comment']) 2 | 3 |
4 |
5 | {{\Illuminate\Support\Str::title($comment->user->name)}} 6 | commented 7 |
8 |
{{$comment->content}}
9 |
10 | @if($comment->published_date_time_formatted) 11 | At {{$comment->published_date_time_formatted}} 12 | @endif 13 | 14 |
15 |
16 | Edit 17 |
18 |
19 | -------------------------------------------------------------------------------- /resources/views/components/backend/comment/publish-status.blade.php: -------------------------------------------------------------------------------- 1 | @props(['comment']) 2 | 3 | @if($comment->is_published) 4 | 6 | Published 7 | 8 | @else 9 | 10 | Not Published 11 | 12 | @endif 13 | -------------------------------------------------------------------------------- /resources/views/components/backend/form/button.blade.php: -------------------------------------------------------------------------------- 1 | @props(['type' => 'submit']) 2 | 7 | -------------------------------------------------------------------------------- /resources/views/components/backend/form/input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['type' => 'text', 'name']) 2 | merge(['class' => 'p-1 border border-indigo-300 rounded focus:outline-none focus:ring-0 focus:border-indigo-500'])}} 4 | > 5 | 6 | @error($name) 7 |
{{$message}}
8 | @enderror 9 | -------------------------------------------------------------------------------- /resources/views/components/backend/form/label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['required' => false]) 2 | 5 | -------------------------------------------------------------------------------- /resources/views/components/backend/form/select.blade.php: -------------------------------------------------------------------------------- 1 | @props(['name']) 2 | 3 | 8 | -------------------------------------------------------------------------------- /resources/views/components/backend/form/textarea.blade.php: -------------------------------------------------------------------------------- 1 | @props(['type' => 'text', 'name', 'rounded' => true]) 2 | 3 | @php 4 | $class = $rounded ? 'rounded' : ''; 5 | @endphp 6 | 7 | 10 | -------------------------------------------------------------------------------- /resources/views/components/backend/navbar/navs.blade.php: -------------------------------------------------------------------------------- 1 | @props(['isMobile' => false]) 2 | 3 | 4 | @php 5 | $menus = [ 6 | 'Articles' => route('backend.article.index'), 7 | 'Comments' => route('backend.comment.index'), 8 | 'Users' => route('backend.user.index'), 9 | 'Categories' => route('backend.category.index'), 10 | 'Keywords' => route('backend.keyword.index'), 11 | 'Feedback' => route('backend.feedback.index'), 12 | 'Subscribers' => route('backend.subscriber.index'), 13 | ]; 14 | @endphp 15 | 16 | @foreach($menus as $menu => $url) 17 | @php 18 | $isActive = request()->url() == $url; 19 | @endphp 20 | 21 | !$isMobile, 24 | 'text-base block' => $isMobile, 25 | 'bg-gray-900 text-white' => $isActive, 26 | 'text-gray-300 hover:bg-gray-700 hover:text-white' => !$isActive, 27 | ]) wire:navigate> 28 | 29 | {{$menu}} 30 | 31 | 32 | 33 | @endforeach 34 | -------------------------------------------------------------------------------- /resources/views/components/backend/reply.blade.php: -------------------------------------------------------------------------------- 1 | @props(['reply']) 2 | 3 |
4 |
5 | {{\Illuminate\Support\Str::title($reply->user->name)}} 7 | replied 8 |
9 |
{{$reply->content}}
10 |
11 | @if($reply->published_date_time_formatted) 12 | At {{$reply->published_date_time_formatted}} 13 | @endif 14 | 15 |
16 |
17 | Edit 18 | Delete 21 |
22 |
23 | -------------------------------------------------------------------------------- /resources/views/components/backend/table.blade.php: -------------------------------------------------------------------------------- 1 |
merge(['class' => 'mt-8 flow-root'])}}> 2 |
3 |
4 |
5 | 6 | 7 | {{$head}} 8 | 9 | 10 | {{$body}} 11 | 12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /resources/views/components/backend/table/td.blade.php: -------------------------------------------------------------------------------- 1 | @props(['wrap' => false]) 2 | merge(['class' => "py-4 pl-4 text-sm".($wrap ? '' : 'whitespace-no-wrap')])}}> 3 | {{$slot}} 4 | 5 | -------------------------------------------------------------------------------- /resources/views/components/backend/table/th.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap"])}}> 3 | {{$slot}} 4 | 5 | -------------------------------------------------------------------------------- /resources/views/components/frontend/article.blade.php: -------------------------------------------------------------------------------- 1 | @props(['article']) 2 | 3 |
4 | 5 | 6 |

7 | 9 | {{$article->heading}} 10 | 11 |

12 | 13 | 14 |
15 | 16 |
17 | {{$article->published_date_formatted}}, on 18 | 20 | {{$article->category->name}} 21 | 22 | by {{$article->user->name}} 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /resources/views/components/frontend/article/comment.blade.php: -------------------------------------------------------------------------------- 1 | @props(['comment']) 2 | 3 |
merge([ 5 | 'class' => "border-b border-blue-300 border-dashed leading-tight py-1" 6 | ]) }}> 7 | 8 |

9 | {{$comment->user->name}} said 10 |

11 |
12 | {{$comment->content}} 13 |
{{$comment->createdAtHuman}}
14 |
15 |
16 | -------------------------------------------------------------------------------- /resources/views/components/frontend/article/comment/form.blade.php: -------------------------------------------------------------------------------- 1 | @props(['article']) 2 | 3 |
4 | @csrf 5 | 14 |
15 | 23 | 32 |
33 | 34 |
35 |
36 | 42 |
43 | 47 |
48 |
49 | -------------------------------------------------------------------------------- /resources/views/components/frontend/article/reply.blade.php: -------------------------------------------------------------------------------- 1 | @props(['reply', 'is_reply' => false,]) 2 | 3 |
merge([ 5 | 'class' => "border-b border-blue-300 border-dashed leading-tight py-1 ml-5" 6 | ]) }}> 7 | 8 |

9 | {{$reply->user->name}} replied 10 |

11 |
12 | {{$reply->content}} 13 |
{{$reply->createdAtHuman}}
14 |
15 |
16 | -------------------------------------------------------------------------------- /resources/views/components/frontend/article/tag.blade.php: -------------------------------------------------------------------------------- 1 | @props(['keyword']) 2 | 11 | {{$keyword->name}} 12 | 13 | -------------------------------------------------------------------------------- /resources/views/components/frontend/article/tags.blade.php: -------------------------------------------------------------------------------- 1 | @props(['keywords']) 2 |
3 | @foreach($keywords as $keyword) 4 | @if(!$loop->first) @endif 5 | 6 | @endforeach 7 |
8 | -------------------------------------------------------------------------------- /resources/views/components/frontend/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | format('Y') . ' Al Imran Ahmed' ?> 5 |
Proudly build with: Larablog
6 |
7 | 8 | 9 | 10 |
11 | Contact 12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/views/components/frontend/google-analytics.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /resources/views/components/frontend/header.blade.php: -------------------------------------------------------------------------------- 1 | @props(['title', 'article' => null]) 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | @vite(['resources/css/app.css', 'resources/js/app.js']) 13 | 14 | 15 | 16 | {{$title}} 17 | 18 | {{$slot}} 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 | -------------------------------------------------------------------------------- /resources/views/components/frontend/meta/facebook.blade.php: -------------------------------------------------------------------------------- 1 | @props(['meta']) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/views/components/frontend/meta/google.blade.php: -------------------------------------------------------------------------------- 1 | @props(['meta']) 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/frontend/meta/twitter.blade.php: -------------------------------------------------------------------------------- 1 | @props(['meta']) 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/components/frontend/seo-meta.blade.php: -------------------------------------------------------------------------------- 1 | @props(['article' => null]) 2 | @if(isset($article)) 3 | @php 4 | $meta = [ 5 | 'author' => $article->user->name, 6 | 'title' => $article->heading, 7 | 'description' => \Illuminate\Support\Arr::get($article->meta, 'description', mb_substr($article->content, 0, 152).'...'), 8 | 'image' => \Illuminate\Support\Arr::get($article->meta, 'image_url', \App\Models\Config::getPath(\App\Models\Config::USER_PHOTO)), 9 | 'url' => route('get-article', $article->slug), 10 | 'site' => url('/'), 11 | 'site_name' => config('app.name'), 12 | 'keywords' => $article->keywords->isEmpty() ? 'imranic-show' : $article->keywords->pluck('name')->implode(', '), 13 | ] 14 | @endphp 15 | 16 | 17 | 18 | @else 19 | 20 | 21 | 22 | @endif 23 | -------------------------------------------------------------------------------- /resources/views/components/frontend/social-links.blade.php: -------------------------------------------------------------------------------- 1 |
merge(['class' => 'flex'])}}> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /resources/views/components/layouts/backend.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | @vite(['resources/css/app.css', 'resources/js/app.js']) 10 | 11 | 12 | 13 | {{$globalConfigs->site_title}} 14 | 15 | @stack('styles') 16 | 17 | 18 | 19 | 20 | 21 |
22 | {{$slot}} 23 |
24 | 25 | @stack('scripts') 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/views/components/layouts/frontend.blade.php: -------------------------------------------------------------------------------- 1 | @props(['title' => $globalConfigs->site_title, 'article' => null]) 2 | 3 | 4 | {{$slot}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/views/components/status.blade.php: -------------------------------------------------------------------------------- 1 | @props(['text', 'state' => 'positive']) 2 | @php 3 | $statusClasses = match ($state) { 4 | 'positive' => 'bg-green-100 text-green-700', 5 | 'negative' => 'bg-red-100 text-red-700', 6 | 'neutral' => 'bg-red-100 text-gray-600', 7 | } 8 | @endphp 9 | 10 | merge(['class' => "inline-flex items-center rounded-full px-2 py-1 text-xs font-medium whitespace-nowrap ".$statusClasses])}}>{{$text}} 11 | -------------------------------------------------------------------------------- /resources/views/components/svg.blade.php: -------------------------------------------------------------------------------- 1 | @props(['icon']) 2 | 3 | @php 4 | $path = match ($icon) { 5 | 'search' => 'M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z', 6 | 'cross' => 'M16.24 14.83a1 1 0 0 1-1.41 1.41L12 13.41l-2.83 2.83a1 1 0 0 1-1.41-1.41L10.59 12 7.76 9.17a1 1 0 0 1 1.41-1.41L12 10.59l2.83-2.83a1 1 0 0 1 1.41 1.41L13.41 12l2.83 2.83z', 7 | 'cross1' => 'M6 18L18 6M6 6l12 12', 8 | 'burger' => 'M4 6h16M4 12h16M4 18h16', 9 | } 10 | @endphp 11 | 12 | 20 | -------------------------------------------------------------------------------- /resources/views/components/toggle.blade.php: -------------------------------------------------------------------------------- 1 | @props(['text' => null, 'isEnabled' => false]) 2 |
3 | 4 | 22 | @if($text) 23 |
{{$text}}
24 | @endif 25 |
26 | -------------------------------------------------------------------------------- /resources/views/emails/comment_confirmation.blade.php: -------------------------------------------------------------------------------- 1 | @extends('emails.master') 2 | @section('content') 3 |

Dear {{$comment->user->name}}

4 | 5 |

A comment has been published on {{$globalConfigs->site_name}} using your email

6 | 7 |

Comment Content:

8 |

{{$comment->content}}

9 | Click here confirm your comment 10 | @endsection -------------------------------------------------------------------------------- /resources/views/emails/comment_thread_notification.blade.php: -------------------------------------------------------------------------------- 1 | @extends('emails.master') 2 | @section('content') 3 |

Hi

4 | 5 |

{{$comment->user->name}} has respond to a comment thread you are following on {{$globalConfigs->site_name}}

6 | 7 |

Response:

8 |

{{$comment->content}}

9 | Click here to see the discussion 10 | @endsection -------------------------------------------------------------------------------- /resources/views/emails/master.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | @yield('content') 6 |
7 |
8 | © {{date('Y').' '.$globalConfigs->copyright_owner}} 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/emails/notify_admin.blade.php: -------------------------------------------------------------------------------- 1 | @extends('emails.master') 2 | @section('content') 3 |

Hi {{$globalConfigs->copyright_owner}}

4 |

A user has respond on {{$globalConfigs->site_name}}

5 | 6 |

Response:

7 | 8 |

{{$comment->content}}

9 | 10 |

Using mail {{$comment->user->email ?? '<>'}}

11 | 12 | Click to see 13 | @endsection 14 | -------------------------------------------------------------------------------- /resources/views/emails/notify_new_article.blade.php: -------------------------------------------------------------------------------- 1 | @extends('emails.master') 2 | @section('content') 3 |

Dear {{$user->name}},

4 |

A new Article has been published in {{$globalConfigs->site_name}} about {{$article->category->name}}

5 | 6 |
7 |

{{$article->heading}}

8 |
{{substr($article->content, 0, 100)}}...
9 | Read More 10 |



11 | 12 | Unsubscribe 13 | @endsection 14 | -------------------------------------------------------------------------------- /resources/views/emails/subscribe_confirmation.blade.php: -------------------------------------------------------------------------------- 1 | @extends('emails.master') 2 | @section('content') 3 |

Dear Reader

4 |

You have expressed your interested to stay connected with {{$globalConfigs->site_name}} 5 | Click the link bellow to confirm


6 | 7 | Click to confirm 8 | 9 | @endsection 10 | -------------------------------------------------------------------------------- /resources/views/emails/testMail.blade.php: -------------------------------------------------------------------------------- 1 | @extends('emails.master') 2 | @section('content) 3 | Welcome 4 | @endsection -------------------------------------------------------------------------------- /resources/views/errors/503.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Be right back. 5 | 6 | 7 | 8 | 39 | 40 | 41 |
42 |
43 |
Be right back.
44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /resources/views/frontend/articles/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | @forelse($articles as $article) 4 | 5 | @empty 6 |
Not available
7 | @endforelse 8 | 9 |
10 | {{method_exists($articles, 'links') ? $articles->links() : ''}} 11 |
12 | 13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /resources/views/frontend/articles/search_result.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | Searched "{{$searched->query}}" 4 |
5 | @forelse($searched->articles as $article) 6 | 7 | @empty 8 |
Not result available
9 | @endforelse 10 | 11 | {{method_exists($searched->articles, 'links') ? $searched->articles->links() : ''}} 12 | 13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /resources/views/frontend/articles/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | {{$article->heading}} 5 |

6 |
7 | Published {{$article->published_date_formatted}} on 8 | 10 | {{$article->category->name}} 11 | 12 | by {{$article->user->name}} 13 |
14 |
15 | {!! $article->htmlContent !!} 16 |
17 | @if(!$article->keywords->isEmpty()) 18 |
19 | 20 |
21 | @endif 22 |
23 | 24 | @if(!$relatedArticles->isEmpty()) 25 |
26 |

27 | More articles on 28 | 30 | {{$article->category->name}} 31 | 32 |

33 | @foreach($relatedArticles as $relatedArticle) 34 | 35 | @endforeach 36 |
37 | @endif 38 | 39 | @if($article->is_comment_enabled) 40 | 41 | @endif 42 | 43 | 44 | 45 |
46 | -------------------------------------------------------------------------------- /resources/views/frontend/contact/create.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/frontend/pages/about.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Al Imran Ahmed 7 |
8 |
9 |

Hi

10 |

I am Al Imran Ahmed

11 |
12 | An experienced web artisan who loves building challenging web application. 13 |
14 | 15 |
16 |
17 |
18 |
19 |

Experienced on

20 |
    21 |
  • - Web Development Workflow
  • 22 |
  • - PHP, Laravel, Livewire
  • 23 |
  • - Javascript, VueJS, Inertia JS
  • 24 |
  • - Tailwind CSS, Bootstrap
  • 25 |
  • - Git
  • 26 |
  • - AWS
  • 27 |
  • - Github, Gitlab and Bitbucket
  • 28 |
  • - JiRA, Trello
  • 29 |
30 |
31 | 32 |
33 |

Current Interests

34 |
    35 |
  • - Building SaaS
  • 36 |
  • - Machine Learning
  • 37 |
  • - Artificial Intelligence
  • 38 |
  • - Blockchain Technology
  • 39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/article/index-row.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 |
10 | On {{$article->categoryName}} 11 |
12 |
13 | At {{$article->created_date_time_formatted}} 14 | in {{ucfirst($article->language)}} 15 |
16 |
17 | @if($article->comment_count > 0) 18 | 19 | {{$article->comment_count.' '.Str::plural('comment', $article->comment_count)}} 20 | 21 | @else 22 | No comment 23 | @endif 24 |
25 |
26 | 27 |
28 | 29 | 32 |
33 |
34 | @if($article->is_published) 35 | {{$article->published_at_human_diff}}
36 | @endif 37 | Updated: {{$article->updated_at_human_diff}} 38 |
39 |
40 | 41 | 42 | Edit 43 | 44 | 46 | Delete 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/category/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Categories

5 |

A list of all the categories.

6 |
7 |
8 | Add New Category 9 |
10 |
11 | 12 | 13 | 14 | Name 15 | Alias 16 | Status 17 | 18 | 19 | 20 | 21 | 22 | @if($adding) 23 |
24 | 25 | 26 | 28 | 29 | 30 | 32 | 33 | 34 | Add 35 | 36 | 37 | 38 | @endif 39 | 40 | @foreach($categories as $category) 41 | 42 | @endforeach 43 |
44 |
45 | 46 |
47 | {{$categories->links()}} 48 |
49 |
50 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/comment/edit.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | Save 13 |
14 |
15 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/comment/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Comments

5 |

A list of all the comments.

6 |
7 |
8 | 9 | 10 | 11 | Comment 12 | By 13 | {{-- Article--}} 14 | Status 15 | 16 | 17 | 18 | 19 | @forelse($comments as $comment) 20 | 21 | @empty 22 | No comment found 23 | @endforelse 24 | 25 | 26 | 27 |
28 | {{$comments->links()}} 29 |
30 |
31 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/comment/show.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | @foreach($comment->replies as $reply) 7 | 8 | @endforeach 9 |
10 |
11 |
12 |
Your Reply
13 | 16 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/config/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Name 6 | Value 7 | 8 | 9 | 10 | @foreach($configs as $config) 11 | 12 | @if($editingConfig && $editingConfig['id'] == $config->id) 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Update 22 | 23 |
24 | @else 25 | {{$config->name}} 26 | {{$config->value}} 27 | 28 | Edit 30 | 31 | @endif 32 | 33 | @endforeach 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/feedback/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @foreach($feedbacks as $feedback) 4 |
5 |
6 |
{{$feedback->name}}
7 | 10 |
11 |
{{$feedback->email}}
12 |
{{$feedback->created_at_human_diff}}
13 | @if($feedback->is_resolved) 14 |
16 | Resolved 17 |
18 | @else 19 |
21 | Not Resolved 22 |
23 | @endif 24 |
{{$feedback->content}}
25 |
26 | @endforeach 27 |
28 | 29 |
30 | {{$feedbacks->links()}} 31 |
32 | 33 | @if($feedbacks->isEmpty()) 34 |
35 | Nothing here! 36 |
37 | @endif 38 |
39 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/keyword/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @foreach($keywords as $keyword) 4 |
5 |
6 |
{{$keyword->name}}
7 | 10 |
11 | 12 | {{$keyword->articles->count()}} {{\Illuminate\Support\Str::plural('article', $keyword->articles->count())}} 13 |
14 | {{$keyword->created_at_human_diff}} 15 |
16 | @endforeach 17 |
18 | 19 |
20 | {{$keywords->links()}} 21 |
22 | 23 | @if($keywords->isEmpty()) 24 |
25 | Nothing here! 26 |
27 | @endif 28 |
29 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/subscriber/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @foreach($subscribers as $subscriber) 4 |
5 | 6 |
{{$subscriber->email}}
7 | 8 | @if($subscriber->verified_at) 9 |
11 | Verified 12 |
13 | @else 14 |
15 | Not Verified 16 |
17 | @endif 18 | 19 |
{{$subscriber->created_date_time_formatted}}
20 | 21 | @if($subscriber->unsubscribed_at) 22 |
Unsubscribed
23 | @endif 24 |
25 | @endforeach 26 |
27 | 28 | @if($subscribers->isEmpty()) 29 |
30 | Nothing here! 31 |
32 | @endif 33 |
34 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/user/form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | @foreach($roles as $role) 7 | 8 | @endforeach 9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | Save 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /resources/views/livewire/backend/user/password-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 | Update Password 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /resources/views/livewire/frontend/article/comments.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | @if($isSubmitted) 5 |
Comment submitted successfully!
6 | @endif 7 |
8 | 9 |
10 | 11 |
12 |

13 | Comments({{$comments->count()}}) 14 |

15 |
+
16 |
17 | 18 | @foreach($comments as $comment) 19 | 20 | @foreach($comment->replies as $reply) 21 | 22 | @endforeach 23 | @endforeach 24 |
25 |
26 | -------------------------------------------------------------------------------- /resources/views/livewire/frontend/subscribe.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($isSubscribed) 3 |
4 | Thanks! You have subscribed successfully. 5 |
6 | @else 7 |
8 | 15 | 18 |
No noise, unsubscribe anytime!
19 |
20 | @endif 21 |
22 | -------------------------------------------------------------------------------- /resources/views/livewire/placeholders/article.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Loading... 17 |
18 | 19 | -------------------------------------------------------------------------------- /resources/views/livewire/placeholders/cards.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @for($i = 1; $i <= 15; $i++) 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | @endfor 14 |
15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /resources/views/livewire/placeholders/default.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Loading... 10 |
11 | 12 | -------------------------------------------------------------------------------- /resources/views/livewire/user/password-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{-- A good traveler has no fixed plans and is not intent upon arriving. --}} 3 |
4 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | monthly(); 7 | Schedule::command(GenerateSitemap::class)->weekly(); 8 | Schedule::command('telescope:prune')->daily(); 9 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('home'); 15 | 16 | Route::get('file/{uuid}', [FileController::class, 'path'])->name('file'); 17 | 18 | //Subscribe 19 | Route::get('subscription/confirm', [SubscriptionController::class, 'confirm'])->name('subscription.confirm'); 20 | Route::get('unsubscribe', [SubscriptionController::class, 'unsubscribe'])->name('unsubscribe'); 21 | 22 | //feedback 23 | Route::view('contact', 'frontend.contact.create')->name('contact'); 24 | 25 | //Article 26 | Route::get('article', [ArticleController::class, 'index'])->name('articles'); 27 | Route::get('article/{slug}', [ArticleController::class, 'show'])->name('get-article'); 28 | Route::get('article/{articleId}/{articleHeading?}', [ArticleController::class, 'showById'])->name('get-article-by-id'); 29 | Route::get('category/article/{categoryAlias}', [CategoryController::class, 'getArticles'])->name('articles-by-category'); 30 | Route::get('keyword/article/{keywordName}', [KeywordController::class, 'getArticles'])->name('articles-by-keyword'); 31 | Route::get('search', [ArticleController::class, 'search'])->name('search-article'); 32 | 33 | //Comment 34 | Route::get('comment/{commentId}/confirm', [CommentController::class, 'confirmComment'])->name('confirm-comment'); 35 | 36 | Route::view('pages/about', 'frontend.pages.about')->name('page.about'); 37 | 38 | //Admin auth 39 | Route::get('admin/login', [AuthController::class, 'showLoginForm'])->name('login-form'); 40 | Route::post('admin/login', [AuthController::class, 'login'])->name('login'); 41 | Route::get('admin/logout', [AuthController::class, 'logout'])->name('logout'); 42 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $uri = urldecode( 9 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 10 | ); 11 | 12 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 13 | // built-in PHP web server. This provides a convenient way to test a Laravel 14 | // application without having installed a "real" web server software here. 15 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 16 | return false; 17 | } 18 | 19 | require_once __DIR__.'/public/index.php'; 20 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | import forms from '@tailwindcss/forms'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: [ 7 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 8 | './storage/framework/views/*.php', 9 | './resources/views/**/*.blade.php', 10 | './resources/js/**/*.vue', 11 | ], 12 | 13 | theme: { 14 | extend: { 15 | fontFamily: { 16 | sans: ['Nunito', ...defaultTheme.fontFamily.sans], 17 | }, 18 | }, 19 | }, 20 | 21 | plugins: [forms], 22 | }; 23 | -------------------------------------------------------------------------------- /tests/Feature/Controllers/CategoryControllerTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $articles = Article::factory()->count(3)->create([ 18 | 'category_id' => $category->id, 19 | 'user_id' => User::factory()->create()->id, 20 | ]); 21 | 22 | $this->get("category/article/{$category->alias}") 23 | ->assertStatus(Response::HTTP_OK) 24 | ->assertViewIs('frontend.articles.index') 25 | ->assertViewHas('articles') 26 | ->assertSee($articles->first()->heading); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Feature/Controllers/FeedbackControllerTest.php: -------------------------------------------------------------------------------- 1 | get('contact') 12 | ->assertStatus(200) 13 | ->assertViewIs('frontend.contact.create'); 14 | 15 | $response->assertStatus(200); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Feature/Controllers/KeywordControllerTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | $category = Category::factory()->create(); 17 | 18 | $article = Article::factory()->published()->create([ 19 | 'heading' => 'Test Heading', 20 | 'category_id' => $category->id, 21 | 'user_id' => $user->id, 22 | ]); 23 | 24 | $keyword = Keyword::factory()->create(); 25 | $article->keywords()->attach($keyword); 26 | 27 | $this->get("keyword/article/{$keyword->name}") 28 | ->assertOk() 29 | ->assertSee($article->heading) 30 | ->assertSee($article->published_at->format('M d, Y')) 31 | ->assertSee("{$article->user->name}") 32 | ->assertSee("{$category->name}"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Feature/Controllers/SubscriptionControllerTest.php: -------------------------------------------------------------------------------- 1 | create(['unsubscribed_at' => null]); 13 | 14 | $this->get("unsubscribe?token={$subscriber->token}")->assertRedirect(); 15 | 16 | $this->assertNotNull($subscriber->fresh()->unsubscribed_at); 17 | } 18 | 19 | public function testConfirmSubscription() 20 | { 21 | $subscriber = Subscriber::factory()->create(); 22 | 23 | $this->get("subscription/confirm?token={$subscriber->token}")->assertRedirect(); 24 | 25 | $this->assertNull($subscriber->fresh()->unsubscribed_at); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/Category/IndexTest.php: -------------------------------------------------------------------------------- 1 | assertViewIs('livewire.backend.category.index') 17 | ->assertViewHas('categories') 18 | ->assertStatus(Response::HTTP_OK); 19 | } 20 | 21 | public function testStartAdding() 22 | { 23 | Livewire::test(Index::class) 24 | ->set('adding', false) 25 | ->call('startAdding') 26 | ->assertSet('adding', true); 27 | } 28 | 29 | public function testStore() 30 | { 31 | Livewire::test(Index::class) 32 | ->set('category', [ 33 | 'name' => $name = Str::random(), 34 | 'alias' => $alias = Str::random(), 35 | ]) 36 | ->call('store'); 37 | 38 | $this->assertDatabaseHas('categories', [ 39 | 'name' => $name, 40 | 'alias' => $alias, 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/Comment/EditTest.php: -------------------------------------------------------------------------------- 1 | create(); 19 | $comment = Comment::factory()->create(['article_id' => $article->id]); 20 | Livewire::test(Edit::class, ['comment' => $comment]) 21 | ->assertViewIs('livewire.backend.comment.edit'); 22 | } 23 | 24 | public function testUpdate() 25 | { 26 | $article = Article::factory()->create(); 27 | $comment = Comment::factory(['article_id' => $article->id])->create(); 28 | $updatedComment = clone $comment; 29 | 30 | Livewire::test(Edit::class, ['comment' => $comment]) 31 | ->set('content', $content = $this->faker->paragraph) 32 | ->call('update', $updatedComment); 33 | 34 | $this->assertDatabaseHas('comments', [ 35 | 'id' => $comment->id, 36 | 'content' => $content, 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/Comment/IndexTest.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create(); 22 | Auth::login($this->user); 23 | } 24 | 25 | public function testRender(): void 26 | { 27 | $article = Article::factory()->create(); 28 | 29 | Comment::factory()->create([ 30 | 'article_id' => $article->id, 31 | 'user_id' => $this->user->id, 32 | ]); 33 | 34 | Livewire::test(Index::class) 35 | ->assertStatus(Response::HTTP_OK); 36 | } 37 | 38 | public function testPlaceholder() 39 | { 40 | Livewire::test(Index::class) 41 | ->call('placeholder') 42 | ->assertStatus(Response::HTTP_OK) 43 | ->assertViewIs('livewire.backend.comment.index') 44 | ->assertViewHas('comments'); 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/Comment/ShowTest.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create(); 28 | Auth::login($this->user); 29 | 30 | $this->article = Article::factory()->create([ 31 | 'user_id' => $this->user->id, 32 | ]); 33 | $this->comment = Comment::factory()->create([ 34 | 'article_id' => $this->article->id, 35 | 'user_id' => $this->user->id, 36 | ]); 37 | } 38 | 39 | public function testRender() 40 | { 41 | Livewire::test(Show::class, ['comment' => $this->comment]) 42 | ->assertOk() 43 | ->assertViewIs('livewire.backend.comment.show'); 44 | } 45 | 46 | public function testSubmitReply() 47 | { 48 | Livewire::test(Show::class, ['comment' => $this->comment]) 49 | ->set('reply', [ 50 | 'content' => $replyContent = $this->faker()->paragraph(), 51 | ]) 52 | ->call('submitReply') 53 | ->assertOk() 54 | ->assertHasNoErrors(); 55 | 56 | $this->assertDatabaseHas('comments', [ 57 | 'parent_comment_id' => $this->comment->id, 58 | 'article_id' => $this->article->id, 59 | 'user_id' => $this->user->id, 60 | 'content' => $replyContent, 61 | ]); 62 | } 63 | 64 | public function testDelete() 65 | { 66 | Livewire::test(Show::class, ['comment' => $this->comment]) 67 | ->call('delete', $this->comment) 68 | ->assertOk() 69 | ->assertHasNoErrors(); 70 | 71 | $this->assertDatabaseMissing('comments', [ 72 | 'id' => $this->comment->id, 73 | ]); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/Config/IndexTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(Response::HTTP_OK) 18 | ->assertViewIs('livewire.backend.config.index') 19 | ->assertViewHas('configs'); 20 | } 21 | 22 | public function testStartEditing() 23 | { 24 | $config = Config::query()->create(['name' => 'test_config', 'value' => Str::random()]); 25 | 26 | Livewire::test(Index::class) 27 | ->call('startEditing', ['config' => $config->id]) 28 | ->assertSee('editingConfig') 29 | ->assertReturned(null); 30 | } 31 | 32 | public function testUpdate() 33 | { 34 | $config = Config::query()->create(['name' => $name = 'test_config', 'value' => Str::random()]); 35 | 36 | Livewire::test(Index::class, ['editingConfig' => []]) 37 | ->set('editingConfig', ['id' => $config->id, 'value' => $value = Str::random()]) 38 | ->call('update', ['config' => $config->id]) 39 | ->assertReturned(null); 40 | 41 | $this->assertDatabaseHas('configs', [ 42 | 'id' => $config->id, 43 | 'name' => $name, 44 | 'value' => $value, 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/DashboardTest.php: -------------------------------------------------------------------------------- 1 | assertOk() 15 | ->assertViewIs('livewire.backend.dashboard') 16 | ->assertViewHas('categories') 17 | ->assertViewHas('latestComments') 18 | ->assertViewHas('latestFeedbacks'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/Feedback/IndexTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(Response::HTTP_OK); 17 | } 18 | 19 | public function testPlaceholder() 20 | { 21 | Livewire::test(Index::class) 22 | ->call('placeholder') 23 | ->assertStatus(Response::HTTP_OK) 24 | ->assertViewIs('livewire.backend.feedback.index') 25 | ->assertViewHas('feedbacks'); 26 | } 27 | 28 | public function testToggleResolvedTrue() 29 | { 30 | $feedback = Feedback::factory()->create(['is_resolved' => false]); 31 | Livewire::test(Index::class) 32 | ->call('toggleResolved', ['feedback' => $feedback->id]); 33 | 34 | $this->assertDatabaseHas('feedbacks', ['id' => $feedback->id, 'is_resolved' => 1]); 35 | } 36 | 37 | public function testToggleResolvedFalse() 38 | { 39 | $feedback = Feedback::factory()->create(['is_resolved' => true]); 40 | 41 | Livewire::test(Index::class) 42 | ->call('toggleResolved', ['feedback' => $feedback->id]); 43 | 44 | $this->assertDatabaseHas('feedbacks', ['id' => $feedback->id, 'is_resolved' => 0]); 45 | } 46 | 47 | public function testClose() 48 | { 49 | $feedback = Feedback::factory()->create(['is_closed' => false]); 50 | 51 | Livewire::test(Index::class) 52 | ->call('close', ['feedback' => $feedback->id]); 53 | 54 | $this->assertDatabaseHas('feedbacks', ['id' => $feedback->id, 'is_closed' => 1]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/Keyword/IndexTest.php: -------------------------------------------------------------------------------- 1 | assertOk() 17 | ->assertViewIs('livewire.backend.keyword.index') 18 | ->assertViewHas('keywords'); 19 | } 20 | 21 | public function testDelete() 22 | { 23 | $keyword = Keyword::factory()->create(); 24 | $article = Article::factory()->create(); 25 | $article->keywords()->attach($keyword); 26 | 27 | Livewire::test(Index::class) 28 | ->call('delete', $keyword) 29 | ->assertOk() 30 | ->assertHasNoErrors(); 31 | 32 | $this->assertDatabaseMissing('keywords', [ 33 | 'id' => $keyword->id, 34 | ]); 35 | 36 | $this->assertDatabaseHas('articles', [ 37 | 'id' => $article->id, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Backend/Subscriber/IndexTest.php: -------------------------------------------------------------------------------- 1 | assertOk(); 14 | } 15 | 16 | public function testPlaceholder() 17 | { 18 | Livewire::test(Index::class) 19 | ->call('placeholder') 20 | ->assertOk() 21 | ->assertViewIs('livewire.backend.subscriber.index'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Frontend/Article/CommentTest.php: -------------------------------------------------------------------------------- 1 | create(); 22 | 23 | Livewire::test(Comments::class, ['article' => $article]) 24 | ->call('add') 25 | ->assertHasErrors('comment.name') 26 | ->assertHasErrors('comment.email') 27 | ->assertHasErrors('comment.content'); 28 | } 29 | 30 | public function testAdd() 31 | { 32 | Mail::fake(); 33 | 34 | Config::query()->create(['name' => 'admin_email', 'value' => 'imran@example.com']); 35 | Role::findOrCreate('reader'); 36 | 37 | $article = Article::factory()->create(); 38 | 39 | Livewire::test(Comments::class, ['article' => $article]) 40 | ->set('comment.content', $content = 'test comment') 41 | ->set('comment.email', $email = 'test@example.com') 42 | ->set('comment.name', $name = 'Al Imran Ahmed') 43 | ->set('comment.notify', 1) 44 | ->call('add'); 45 | 46 | Mail::assertQueued(CommentConfirmation::class); 47 | Mail::assertQueued(NotifyAdmin::class); 48 | 49 | $comment = Comment::query()->where('article_id', $article->id)->where('content', $content)->first(); 50 | $this->assertNotNull($comment); 51 | $this->assertEquals(0, $comment->is_published); 52 | $this->assertEquals(0, $comment->is_confirmed); 53 | 54 | $user = User::query()->where('email', $email)->first(); 55 | $this->assertNotNull($user); 56 | $this->assertEquals($name, $user->name); 57 | 58 | $this->assertEquals(1, $user->reader->notify); 59 | $this->assertEquals(0, $user->reader->is_verified); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Frontend/ContactFormTest.php: -------------------------------------------------------------------------------- 1 | 'admin_email', 'value' => 'imran@example.com']); 18 | 19 | Livewire::test(ContactForm::class) 20 | ->set('name', $name = 'Test User') 21 | ->set('email', $email = 'test@example.com') 22 | ->set('content', $content = 'Test feedback') 23 | ->call('submit'); 24 | 25 | $this->assertDatabaseHas('feedbacks', compact('name', 'email', 'content')); 26 | 27 | Mail::assertQueued(NotifyAdmin::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Feature/Livewire/Frontend/SubscribeTest.php: -------------------------------------------------------------------------------- 1 | set('email', $email = 'imran@gmail.com') 19 | ->call('subscribe'); 20 | 21 | $this->assertDatabaseHas('subscribers', [ 22 | 'email' => $email, 23 | 'unsubscribed_at' => null, 24 | ]); 25 | 26 | Mail::assertQueued(SubscribeConfirmation::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create(['email' => 'example@test.com']); 29 | 30 | $this->category = Category::factory()->create(); 31 | } 32 | 33 | public function testPublishedScope() 34 | { 35 | Article::factory()->published()->create([ 36 | 'user_id' => $this->user->id, 37 | 'category_id' => $this->category->id, 38 | ]); 39 | $this->assertEquals(1, Article::published()->value('is_published')); 40 | } 41 | 42 | public function testNotDeletedScope() 43 | { 44 | Article::factory()->create([ 45 | 'is_deleted' => 0, 46 | 'user_id' => $this->user->id, 47 | 'category_id' => $this->category->id, 48 | ]); 49 | 50 | $article = Article::notDeleted()->first(); 51 | $this->assertEquals(0, $article->is_deleted); 52 | } 53 | 54 | public function testCategoryNameAttribute() 55 | { 56 | $category = Category::factory()->create(['name' => 'Test Category']); 57 | $article = Article::factory()->create([ 58 | 'user_id' => $this->user->id, 59 | 'category_id' => $category->id, 60 | ]); 61 | 62 | $this->assertEquals($category->name, $article->categoryName); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Unit/Models/CategoryTest.php: -------------------------------------------------------------------------------- 1 | create(['is_active' => 1]); 18 | $category = Category::active()->first(); 19 | $this->assertEquals(1, $category->is_active); 20 | } 21 | 22 | public function testNonEmptyOnly() 23 | { 24 | $user = User::factory()->create(['email' => 'example@test.com']); 25 | 26 | $category = Category::factory()->create(); 27 | Article::factory()->create(['category_id' => $category->id, 'user_id' => $user->id]); 28 | 29 | $this->assertTrue(Category::getNonEmptyOnly()->first()->articles->isNotEmpty()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/Models/CommentTest.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create(['email' => 'example@test.com']); 36 | 37 | $this->category = Category::factory()->create(); 38 | 39 | $this->article = Article::factory()->create([ 40 | 'user_id' => $this->user->id, 41 | 'category_id' => $this->category->id, 42 | ]); 43 | } 44 | 45 | public function testPublishedScope() 46 | { 47 | Comment::factory()->create([ 48 | 'user_id' => $this->user->id, 49 | 'article_id' => $this->article->id, 50 | 'is_published' => 1, 51 | ]); 52 | 53 | $comment = Comment::published()->first(); 54 | $this->assertEquals(1, $comment->is_published); 55 | } 56 | 57 | public function testNoRepliesTest() 58 | { 59 | Comment::factory()->create([ 60 | 'user_id' => $this->user->id, 61 | 'article_id' => $this->article->id, 62 | 'is_published' => 1, 63 | 'parent_comment_id' => null, 64 | ]); 65 | 66 | $comment = Comment::noReplies()->first(); 67 | 68 | $this->assertNull($comment->parentComment); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Unit/Models/ConfigTest.php: -------------------------------------------------------------------------------- 1 | 'test_name', 'value' => 'Test Value']); 16 | 17 | $this->assertEquals('Test Value', Config::get('test_name')); 18 | 19 | $this->assertNull(Config::get('abc_xyz')); 20 | } 21 | 22 | public function testAllFormatted() 23 | { 24 | $name1 = 'test_name1'; 25 | $name2 = 'test_name2'; 26 | $value1 = 'Test Value1'; 27 | $value2 = 'Test Value2'; 28 | 29 | Config::create(['name' => $name1, 'value' => $value1, 'is_active' => 1]); 30 | Config::create(['name' => $name2, 'value' => $value2, 'is_active' => 0]); 31 | 32 | $config = Config::allFormatted(1); 33 | 34 | $this->assertEquals($value1, $config->$name1); 35 | 36 | try { 37 | $this->assertNotEquals('Test Value1', $this->$name2); 38 | } catch (\ErrorException $e) { 39 | $this->expectExceptionMessage('Undefined property: stdClass::$test_name1'); 40 | } 41 | 42 | $config = Config::allFormatted(0); 43 | 44 | $this->assertEquals($value1, $config->$name1); 45 | $this->assertEquals($value2, $this->$name2); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Unit/Models/KeywordTest.php: -------------------------------------------------------------------------------- 1 | create(['email' => 'example@test.com']); 23 | 24 | $category = Category::factory()->create(); 25 | 26 | $this->article = Article::factory()->create([ 27 | 'user_id' => $user->id, 28 | 'category_id' => $category->id, 29 | ]); 30 | } 31 | 32 | public function testGetArticleIds() 33 | { 34 | $keyword = Keyword::factory()->count(2)->create(); 35 | 36 | $this->article->keywords()->attach($keyword->first()->id); 37 | 38 | $this->assertSame([$this->article->id], Keyword::getArticleIDs($keyword)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Unit/Models/ReaderTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | Reader::create([ 19 | 'user_id' => $user->id, 20 | 'is_verified' => 1, 21 | ]); 22 | 23 | $this->assertEquals(1, Reader::verified()->value('is_verified')); 24 | } 25 | 26 | public function testSubscribedScope() 27 | { 28 | $user = User::factory()->create(); 29 | 30 | Reader::create([ 31 | 'user_id' => $user->id, 32 | 'notify' => 1, 33 | ]); 34 | 35 | $this->assertEquals(1, Reader::subscribed()->value('notify')); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/coverage/badge-coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | coverage 10 | coverage 11 | 12 | 13 | 72 % 14 | 72 % 15 | 16 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | laravel({ 7 | input: [ 8 | 'resources/css/app.css', 9 | 'resources/js/app.js', 10 | ], 11 | refresh: true, 12 | }), 13 | { 14 | name: 'blade', 15 | handleHotUpdate({ file, server }) { 16 | if (file.endsWith('.blade.php')) { 17 | server.ws.send({ 18 | type: 'full-reload', 19 | path: '*', 20 | }); 21 | } 22 | }, 23 | } 24 | ], 25 | }); 26 | --------------------------------------------------------------------------------