├── .DS_Store ├── .gitignore ├── .idea ├── .gitignore ├── filament-blog.iml ├── modules.xml ├── php-test-framework.xml ├── php.xml └── vcs.xml ├── .phpunit.result.cache ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── composer.json ├── composer.lock ├── config └── filamentblog.php ├── database ├── factories │ ├── CategoryFactory.php │ ├── CategoryPostFactory.php │ ├── CommentFactory.php │ ├── NewsLetterFactory.php │ ├── PostFactory.php │ ├── PostTagFactory.php │ ├── SeoDetailFactory.php │ ├── SettingFactory.php │ ├── ShareSnippetFactory.php │ ├── TagFactory.php │ └── UserFactory.php └── migrations │ ├── 2024_05_11_152936_create_add_prefix_on_all_blog_tables.php │ └── create_blog_tables.php.stub ├── images └── landing.png ├── phpunit.xml ├── resources └── views │ ├── blogs │ ├── all-post.blade.php │ ├── category-post.blade.php │ ├── index.blade.php │ ├── search.blade.php │ ├── show.blade.php │ └── tag-post.blade.php │ ├── components │ ├── card.blade.php │ ├── comment.blade.php │ ├── feature-card.blade.php │ ├── header-category.blade.php │ ├── header.blade.php │ └── recent-post.blade.php │ ├── layouts │ └── app.blade.php │ ├── mails │ └── blog-published.blade.php │ └── tables │ └── columns │ └── user-photo-name.blade.php ├── routes └── web.php ├── src ├── Blog.php ├── Components │ ├── Card.php │ ├── Comment.php │ ├── FeatureCard.php │ ├── Header.php │ ├── HeaderCategory.php │ ├── Layout.php │ └── RecentPost.php ├── Concerns │ └── HasCategories.php ├── Console │ └── Commands │ │ └── RenameTablesCommand.php ├── Enums │ └── PostStatus.php ├── EventServiceProvider.php ├── Events │ └── BlogPublished.php ├── Exceptions │ └── CannotSendEmail.php ├── Facades │ └── SEOMeta.php ├── FilamentBlogServiceProvider.php ├── Http │ └── Controllers │ │ ├── CategoryController.php │ │ ├── CommentController.php │ │ ├── Controller.php │ │ ├── PostController.php │ │ └── TagController.php ├── Jobs │ └── PostScheduleJob.php ├── Listeners │ └── SendBlogPublishedNotification.php ├── Mails │ └── BlogPublished.php ├── Models │ ├── Category.php │ ├── CategoryPost.php │ ├── Comment.php │ ├── NewsLetter.php │ ├── Post.php │ ├── PostTag.php │ ├── SeoDetail.php │ ├── Setting.php │ ├── ShareSnippet.php │ ├── Tag.php │ └── User.php ├── Resources │ ├── CategoryResource.php │ ├── CategoryResource │ │ ├── Pages │ │ │ ├── CreateCategory.php │ │ │ ├── EditCategory.php │ │ │ ├── ListCategories.php │ │ │ └── ViewCategory.php │ │ └── RelationManagers │ │ │ └── PostsRelationManager.php │ ├── CommentResource.php │ ├── CommentResource │ │ └── Pages │ │ │ ├── CreateComment.php │ │ │ ├── EditComment.php │ │ │ └── ListComments.php │ ├── NewsletterResource.php │ ├── NewsletterResource │ │ └── Pages │ │ │ ├── CreateNewsletter.php │ │ │ ├── EditNewsletter.php │ │ │ └── ListNewsletters.php │ ├── PostResource.php │ ├── PostResource │ │ ├── Pages │ │ │ ├── CreatePost.php │ │ │ ├── EditPost.php │ │ │ ├── ListPosts.php │ │ │ ├── ManaePostSeoDetail.php │ │ │ ├── ManagePostComments.php │ │ │ └── ViewPost.php │ │ ├── RelationManagers │ │ │ ├── CommentsRelationManager.php │ │ │ └── SeoDetailRelationManager.php │ │ └── Widgets │ │ │ └── BlogPostPublishedChart.php │ ├── SeoDetailResource.php │ ├── SeoDetailResource │ │ └── Pages │ │ │ ├── CreateSeoDetail.php │ │ │ ├── EditSeoDetail.php │ │ │ └── ListSeoDetails.php │ ├── SettingResource.php │ ├── SettingResource │ │ └── Pages │ │ │ ├── CreateSetting.php │ │ │ ├── EditSetting.php │ │ │ └── ListSettings.php │ ├── ShareSnippetResource.php │ ├── ShareSnippetResource │ │ └── Pages │ │ │ ├── CreateShareSnippet.php │ │ │ ├── EditShareSnippet.php │ │ │ └── ListShareSnippets.php │ ├── TagResource.php │ └── TagResource │ │ └── Pages │ │ ├── CreateTag.php │ │ ├── EditTag.php │ │ └── ListTags.php ├── SEOMeta.php ├── Services │ └── SEOService.php ├── Tables │ └── Columns │ │ └── UserPhotoName.php └── Traits │ └── HasBlog.php └── tests ├── Feature ├── ExampleTest.php ├── Models │ ├── CategoryTest.php │ ├── CommentTest.php │ ├── PostTest.php │ ├── SeoDeatilTest.php │ └── TagTest.php ├── NewPostPublishedMailTest.php ├── Pages │ ├── AllPageTest.php │ ├── CategoryPageTest.php │ ├── HomePageTest.php │ ├── PostDetailsPageTest.php │ ├── SearchPageTest.php │ └── TagPageTest.php ├── UserCommentTest.php └── UserNewsLetterSubscriptionTest.php ├── Pest.php ├── TestCase.php └── Unit └── ExampleTest.php /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefireflytech/filament-blog/5a60ee57dd2785f8eb84a7a184240e229b59640d/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /database/migrations/*.php 3 | !/database/migrations/2024_05_11_152936_create_add_prefix_on_all_blog_tables.php 4 | .github 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/php-test-framework.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"Firefly\\FilamentBlog\\Tests\\Integration\\DemoTest::test_demo":8,"Firefly\\FilamentBlog\\Tests\\Integration\\DemoTest::test_route":7,"Firefly\\FilamentBlog\\Tests\\Feature\\DemoTest::test_route":8},"times":{"Firefly\\FilamentBlog\\Tests\\Integration\\DemoTest::test_demo":0.026,"Firefly\\FilamentBlog\\Tests\\Integration\\DemoTest::test_route":0.087,"Firefly\\FilamentBlog\\Tests\\Feature\\DemoTest::test_route":0.055}} -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. Please read and understand the contribution guide before creating an issue or pull request. Contributions are accepted via Pull Requests on [Github](https://github.com/thefireflytech/filament-blog). 4 | 5 | ### Etiquette 6 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work. 7 | 8 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people. 9 | 10 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 11 | 12 | ### Viability 13 | When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs from your own. Think about whether or not your feature is likely to be used by other users of the project. 14 | 15 | ## Issues 16 | Issue reports are warmly welcomed, as we know it's important aspect of open source software. Whether it's a bug or a possible enhancement, We are aware that it will improve and build trust among the community. Before filing an issue, please consider the following things. 17 | 18 | - You can add an issue in the [issues section](https://github.com/thefireflytech/filament-blog/issues) 19 | - Make sure the same issue is not reported by any other person. 20 | - Add a decent title and sufficient description of the issue, you may also add some screenshots. 21 | - Please consider adding label to issue that allows us to understand nature of the issue. 22 | - Please consider adding fixes via pull requests if you are aware of the root cause of the issue and a possible solution. 23 | - For enhancement, please provide the necessity of the feature, the problem it solves and what it offers as an improvement in the library. 24 | - Please consider adding a response if the answer added on the discussion solves the issue. 25 | 26 | ## Pull Requests 27 | 28 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 29 | 30 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. 31 | 32 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 33 | 34 | - **Descriptive Branch Name** - Please add descriptive branch name that aligns with the implementation 35 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 36 | - **Which Branch** - All bug fixes and new features should always be sent to the master branch, which contains the upcoming release. 37 | 38 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 39 | 40 | 41 | **Happy coding**! 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Firefly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), for personal use only. 7 | Personal use includes the use, copy, modification, merge, publication, and 8 | distribution of the Software. Any form of commercial use, including but not limited 9 | to selling the package or modifying it for selling purposes, is strictly prohibited. 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firefly Filament Blog 2 | The Filament Blog Plugin is a feature-rich plugin designed to enhance your blogging experience on your website. It comes with a variety of powerful features to help you manage and customize your blog posts effectively. 3 | 4 | [![Latest Version on Packagist][ico-version]][link-packagist] 5 | [![Total Downloads][ico-downloads]][link-downloads] 6 | ![Packagist License][ico-license] 7 | ![GitHub forks][ico-forks] 8 | ![GitHub Org's stars][ico-stars] 9 | 10 | 11 | 12 | ![Firefly Filament Blog](https://raw.githubusercontent.com/thefireflytech/filament-blog/master/images/landing.png) 13 | 14 | ## Features 15 | 16 | - **Easy Installation:** Simple and straightforward installation process. 17 | - **User-Friendly Interface:** Intuitive and user-friendly interface for easy management of blog posts. 18 | - **SEO Meta Extension:** Enhance your blog's search engine optimization with built-in meta tag customization. 19 | - **Post Scheduled for Future:** Schedule your blog posts to be published at a future date and time. 20 | - **Social Media Share Feature:** Allow users to easily share your blog posts on social media platforms. 21 | - **Comment Feature:** Enable comments on your blog posts to encourage engagement and discussion. 22 | - **Newsletter Subscription:** Integrate newsletter subscription forms to grow your email list. 23 | - **New Post Published Notification:** Notify subscribers when a new blog post is published. 24 | - **Category Search:** Categorize your blog posts for easy navigation and search. 25 | - **Support**: [Laravel 11](https://laravel.com) and [Filament 3.x](https://filamentphp.com) 26 | 27 | ## Demo Video 28 | [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/8UkcAicQZUc/0.jpg)](https://www.youtube.com/watch?v=8UkcAicQZUc) 29 | 30 | ## Upgrade Note 31 | >Important: If you are upgrading from version 1.x to 2.x, please follow the steps below: 32 | > - Backup your database before running the migration. This is just for safety purposes. 33 | > - Now you can add prefix on blog tables from the config file. 34 | ```php 35 | 'tables' => [ 36 | 'prefix' => 'fblog_', // prefix for all blog tables 37 | ], 38 | ``` 39 | > - After set the prefix please run the migration by running the following command: 40 | ``php artisan filament-blog:upgrade-tables`` 41 | ## Installation 42 | If your project is not already using Filament, you can install it by running the following commands: 43 | ```bash 44 | composer require filament/filament:"^3.2" -W 45 | ``` 46 | ```bash 47 | php artisan filament:install --panels 48 | ``` 49 | Install the Filament Blog Plugin by running the following command: 50 | ```bash 51 | composer require firefly/filament-blog 52 | ``` 53 | 54 | ## Usage 55 | After composer require, you can start using the Filament Blog Plugin by runing the following command: 56 | 57 | ```bash 58 | php artisan filament-blog:install 59 | ``` 60 | This command will publish `filamentblog.php` config file and `create_blog_tables.php` migration file. 61 | ````php 62 | [ 79 | 'prefix' => 'fblog_', // prefix for all blog tables 80 | ], 81 | 'route' => [ 82 | 'prefix' => 'blogs', 83 | 'middleware' => ['web'], 84 | // 'home' => [ 85 | // 'name' => 'filamentblog.home', 86 | // 'url' => env('APP_URL'), 87 | // ], 88 | 'login' => [ 89 | 'name' => 'filamentblog.post.login', 90 | ], 91 | ], 92 | 'user' => [ 93 | 'model' => User::class, 94 | 'foreign_key' => 'user_id', 95 | 'columns' => [ 96 | 'name' => 'name', 97 | 'avatar' => 'profile_photo_path', // column name for avatar 98 | ], 99 | ], 100 | 'seo' => [ 101 | 'meta' => [ 102 | 'title' => 'Filament Blog', 103 | 'description' => 'This is filament blog seo meta description', 104 | 'keywords' => [], 105 | ], 106 | ], 107 | 108 | 'recaptcha' => [ 109 | 'enabled' => false, // true or false 110 | 'site_key' => env('RECAPTCHA_SITE_KEY'), 111 | 'secret_key' => env('RECAPTCHA_SECRET_KEY'), 112 | ], 113 | ]; 114 | ```` 115 | If you have a different url for the home page, you can set it in the `home` key in the `route` configuration. 116 | Before running the migration, you can modify the `filamentblog.php` config file to suit your needs. 117 | 118 | If you want to publish config, views, components, and migrations individually you can run the following command: 119 | ```bash 120 | php artisan vendor:publish --provider="Firefly\FilamentBlog\FilamentBlogServiceProvider" --tag=filament-blog-views 121 | ``` 122 | ```bash 123 | php artisan vendor:publish --provider="Firefly\FilamentBlog\FilamentBlogServiceProvider" --tag=filament-blog-config 124 | ``` 125 | ```bash 126 | php artisan vendor:publish --provider="Firefly\FilamentBlog\FilamentBlogServiceProvider" --tag=filament-blog-components 127 | ``` 128 | ```bash 129 | php artisan vendor:publish --provider="Firefly\FilamentBlog\FilamentBlogServiceProvider" --tag=filament-blog-migrations 130 | ``` 131 | 132 | ## What if you have already a User model? 133 | - If you already have a User model, you can modify the `filamentblog.php` config file to use your User model. 134 | - Make sure the name column is the user's `name` column. 135 | - If you have already `avatar` column in your User model, you can set it in the `filamentblog.php` config file in `user.columns.avatar` key. 136 | - If you want to change `foreign_key` column name, you can modify the `filamentblog.php` config file. 137 | 138 | ## Migrate the database 139 | After modifying the `filamentblog.php` config file, you can run the migration by running the following command: 140 | ```bash 141 | php artisan migrate 142 | ``` 143 | ## Storage Link 144 | After running the migration, you can create a symbolic link to the storage directory by running the following command: 145 | ```bash 146 | php artisan storage:link 147 | ``` 148 | ## Attach filament blog panel to the dashboard 149 | You can attach the Filament Blog panel to the dashboard by adding the following code to your panel provider: 150 | Add `Blog::make()` to your panel passing the class to your `plugins()` method. 151 | 152 | ```php 153 | use Firefly\FilamentBlog\Blog; 154 | 155 | public function panel(Panel $panel): Panel 156 | { 157 | return $panel 158 | ->plugins([ 159 | Blog::make() 160 | ]) 161 | } 162 | ``` 163 | 164 | ## Manage user relationship 165 | If you want to manage the user relationship, you can modify the `User` model to have a relationship with the `Post` model. 166 | ```php 167 | Made with love by Firefly IT Solutions, Nepal - [thefireflytech.com](https://thefireflytech.com) 237 | 238 | 239 | [ico-version]: https://img.shields.io/packagist/v/firefly/filament-blog.svg?style=flat-square 240 | [ico-downloads]: https://img.shields.io/packagist/dt/firefly/filament-blog.svg?style=flat-square 241 | [ico-stable]: https://img.shields.io/packagist/s/firefly/filament-blog.svg?style=flat-square 242 | [ico-license]: https://img.shields.io/packagist/l/firefly/filament-blog.svg?style=flat-square 243 | [ico-forks]: https://img.shields.io/github/forks/thefireflytech/filament-blog.svg?style=flat-square 244 | [ico-stars]: https://img.shields.io/github/stars/thefireflytech?style=flat-square 245 | 246 | 247 | [link-packagist]: https://packagist.org/packages/firefly/filament-blog 248 | [link-downloads]: https://packagist.org/packages/firefly/filament-blog 249 | [link-author]: https://github.com/thefireflytech 250 | [link-asmit]: https://github.com/AsmitNepali 251 | [link-contributors]: ../../contributors 252 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firefly/filament-blog", 3 | "type": "library", 4 | "description": "An advance blog package for Filament Admin Panel", 5 | "keywords": [ 6 | "filament", 7 | "laravel", 8 | "blog", 9 | "firefly" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Asmit Nepali", 15 | "email": "asmit@thefireflytech.com", 16 | "homepage": "https://github.com/asmit-firefly", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=8.0", 22 | "filament/support": "^3.2", 23 | "awcodes/filament-tiptap-editor": "^3.2" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Firefly\\FilamentBlog\\": "src/", 28 | "Firefly\\FilamentBlog\\Database\\Factories\\": "database/factories/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Firefly\\FilamentBlog\\Tests\\": "tests" 34 | } 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "Firefly\\FilamentBlog\\FilamentBlogServiceProvider" 40 | ] 41 | } 42 | }, 43 | "require-dev": { 44 | "orchestra/testbench": "^9", 45 | "pestphp/pest": "2.34.5", 46 | "pestphp/pest-plugin-laravel": "^2.3" 47 | }, 48 | "minimum-stability": "dev", 49 | "prefer-stable": true, 50 | "config": { 51 | "allow-plugins": { 52 | "pestphp/pest-plugin": true 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /config/filamentblog.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'prefix' => 'fblog_', // prefix for all blog tables 19 | ], 20 | 'route' => [ 21 | 'prefix' => 'blogs', 22 | 'middleware' => ['web'], 23 | // 'home' => [ 24 | // 'name' => 'filamentblog.home', 25 | // 'url' => env('APP_URL'), 26 | // ], 27 | 'login' => [ 28 | 'name' => 'filamentblog.post.login', 29 | ], 30 | ], 31 | 'user' => [ 32 | 'model' => User::class, 33 | 'foreign_key' => 'user_id', 34 | 'columns' => [ 35 | 'name' => 'name', 36 | 'avatar' => 'profile_photo_path', // column name for avatar 37 | ], 38 | ], 39 | 'seo' => [ 40 | 'meta' => [ 41 | 'title' => 'Filament Blog', 42 | 'description' => 'This is filament blog seo meta description', 43 | 'keywords' => [], 44 | ], 45 | ], 46 | 47 | 'recaptcha' => [ 48 | 'enabled' => false, // true or false 49 | 'site_key' => env('RECAPTCHA_SITE_KEY'), 50 | 'secret_key' => env('RECAPTCHA_SECRET_KEY'), 51 | ], 52 | ]; 53 | -------------------------------------------------------------------------------- /database/factories/CategoryFactory.php: -------------------------------------------------------------------------------- 1 | $name = $this->faker->word, 25 | 'slug' => Str::slug($name), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/factories/CategoryPostFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->randomNumber(), 24 | 'category_id' => $this->faker->randomNumber(), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/factories/CommentFactory.php: -------------------------------------------------------------------------------- 1 | User::factory(), 25 | 'comment' => $this->faker->word, 26 | 'approved' => false, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/factories/NewsLetterFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->safeEmail(), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/factories/PostFactory.php: -------------------------------------------------------------------------------- 1 | $title = $this->faker->sentence(4), 28 | 'slug' => Str::slug($title), 29 | 'sub_title' => $this->faker->word(), 30 | 'body' => $this->faker->text(), 31 | 'status' => PostStatus::PENDING, 32 | 'published_at' => $this->faker->dateTime(), 33 | 'scheduled_for' => $this->faker->dateTime(), 34 | 'cover_photo_path' => $this->faker->imageUrl(), 35 | 'photo_alt_text' => $this->faker->word, 36 | 'user_id' => User::factory(), 37 | ]; 38 | } 39 | 40 | public function published(?Carbon $date = null): PostFactory 41 | { 42 | return $this->state(fn ($attribute) => [ 43 | 'status' => PostStatus::PUBLISHED, 44 | 'published_at' => $date ?? Carbon::now(), 45 | ]); 46 | } 47 | 48 | public function pending(): PostFactory 49 | { 50 | return $this->state(fn ($attribute) => [ 51 | 'status' => PostStatus::PENDING, 52 | ]); 53 | } 54 | 55 | public function scheduled(?Carbon $date = null): PostFactory 56 | { 57 | return $this->state(fn ($attribute) => [ 58 | 'status' => PostStatus::SCHEDULED, 59 | 'scheduled_for' => $date ?? Carbon::now(), 60 | ]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /database/factories/PostTagFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->randomNumber(), 24 | 'tag_id' => $this->faker->randomNumber(), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/factories/SeoDetailFactory.php: -------------------------------------------------------------------------------- 1 | faker->randomElements(SeoDetail::KEYWORDS, 3); 24 | 25 | return [ 26 | 'post_id' => Post::factory(), 27 | 'title' => $this->faker->sentence(4), 28 | 'keywords' => $keywords, 29 | 'description' => $this->faker->sentence(1), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/factories/SettingFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class SettingFactory extends Factory 12 | { 13 | 14 | /** 15 | * The name of the factory's corresponding model. 16 | * 17 | * @var string 18 | */ 19 | protected $model = Setting::class; 20 | 21 | /** 22 | * Define the model's default state. 23 | * 24 | * @return array 25 | */ 26 | public function definition(): array 27 | { 28 | return [ 29 | 'title' => $this->faker->sentence, 30 | 'description' => $this->faker->paragraph, 31 | 'logo' => $this->faker->imageUrl(), 32 | 'favicon' => $this->faker->imageUrl(), 33 | 'organization_name' => $this->faker->company, 34 | 'google_console_code' => '', 35 | 'google_analytic_code' => '', 36 | 'google_adsense_code' => '', 37 | 'quick_links' => [ 38 | [ 39 | 'label' => 'Home', 40 | 'url' => $this->faker->url, 41 | ], 42 | [ 43 | 'label' => 'About', 44 | 'url' => $this->faker->url, 45 | ], 46 | [ 47 | 'label' => 'Contact', 48 | 'url' => $this->faker->url, 49 | ], 50 | ], 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /database/factories/ShareSnippetFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ShareSnippetFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'script_code' => 'Please paste your script here.', 21 | 'html_code' => 'Please paste your html here.', 22 | 'active' => false, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/factories/TagFactory.php: -------------------------------------------------------------------------------- 1 | $name = $this->faker->word(), 25 | 'slug' => Str::slug($name), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class UserFactory extends Factory 14 | { 15 | /** 16 | * The current password being used by the factory. 17 | */ 18 | protected static ?string $password; 19 | 20 | protected $model = User::class; 21 | 22 | /** 23 | * Define the model's default state. 24 | * 25 | * @return array 26 | */ 27 | public function definition(): array 28 | { 29 | 30 | return [ 31 | 'name' => fake()->name(), 32 | 'email' => fake()->unique()->safeEmail(), 33 | 'email_verified_at' => now(), 34 | 'password' => static::$password ??= Hash::make('password'), 35 | 'remember_token' => Str::random(10), 36 | ]; 37 | } 38 | 39 | /** 40 | * Indicate that the model's email address should be unverified. 41 | */ 42 | public function unverified(): static 43 | { 44 | return $this->state(fn (array $attributes) => [ 45 | 'email_verified_at' => null, 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /database/migrations/2024_05_11_152936_create_add_prefix_on_all_blog_tables.php: -------------------------------------------------------------------------------- 1 | foreignIdFor(config('filamentblog.user.model'), config('filamentblog.user.foreign_key')) 24 | ->change() 25 | ->constrained() 26 | ->cascadeOnDelete(); 27 | }); 28 | 29 | Schema::table(config('filamentblog.tables.prefix').'category_'.config('filamentblog.tables.prefix').'post', function (Blueprint $table) { 30 | $table->foreignIdFor(Firefly\FilamentBlog\Models\Post::class) 31 | ->change() 32 | ->constrained(config('filamentblog.tables.prefix').'posts') 33 | ->cascadeOnDelete(); 34 | $table->foreignIdFor(Firefly\FilamentBlog\Models\Category::class) 35 | ->change() 36 | ->constrained(config('filamentblog.tables.prefix').'categories') 37 | ->cascadeOnDelete(); 38 | }); 39 | 40 | Schema::table(config('filamentblog.tables.prefix').'seo_details', function (Blueprint $table) { 41 | $table->foreignIdFor(Firefly\FilamentBlog\Models\Post::class) 42 | ->change() 43 | ->constrained(config('filamentblog.tables.prefix').'posts') 44 | ->cascadeOnDelete(); 45 | }); 46 | 47 | Schema::table(config('filamentblog.tables.prefix').'comments', function (Blueprint $table) { 48 | $table->foreignIdFor(config('filamentblog.user.model'), config('filamentblog.user.foreign_key')) 49 | ->change() 50 | ->constrained() 51 | ->cascadeOnDelete(); 52 | $table->foreignIdFor(Firefly\FilamentBlog\Models\Post::class) 53 | ->change() 54 | ->constrained(config('filamentblog.tables.prefix').'posts') 55 | ->cascadeOnDelete(); 56 | }); 57 | 58 | Schema::table(config('filamentblog.tables.prefix').'post_'.config('filamentblog.tables.prefix').'tag', function (Blueprint $table) { 59 | $table->foreignIdFor(Firefly\FilamentBlog\Models\Post::class) 60 | ->change() 61 | ->constrained(config('filamentblog.tables.prefix').'posts') 62 | ->cascadeOnDelete(); 63 | $table->foreignIdFor(Firefly\FilamentBlog\Models\Tag::class) 64 | ->change() 65 | ->constrained(config('filamentblog.tables.prefix').'tags') 66 | ->cascadeOnDelete(); 67 | }); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /database/migrations/create_blog_tables.php.stub: -------------------------------------------------------------------------------- 1 | getTable(); 17 | $columnName = config('filamentblog.user.columns.avatar'); 18 | 19 | Schema::create(config('filamentblog.tables.prefix').'categories', function (Blueprint $table) { 20 | $table->id(); 21 | $table->string('name', 155)->unique(); 22 | $table->string('slug', 155)->unique(); 23 | $table->timestamps(); 24 | }); 25 | 26 | Schema::create(config('filamentblog.tables.prefix').'posts', function (Blueprint $table) { 27 | $table->id(); 28 | $table->string('title'); 29 | $table->string('slug'); 30 | $table->string('sub_title')->nullable(); 31 | $table->longText('body'); 32 | $table->enum('status', ['published', 'scheduled', 'pending'])->default('pending'); 33 | $table->dateTime('published_at')->nullable(); 34 | $table->dateTime('scheduled_for')->nullable(); 35 | $table->string('cover_photo_path'); 36 | $table->string('photo_alt_text'); 37 | $table->foreignId(config('filamentblog.user.foreign_key')) 38 | ->constrained() 39 | ->cascadeOnDelete(); 40 | $table->timestamps(); 41 | }); 42 | 43 | Schema::create(config('filamentblog.tables.prefix').'category_'.config('filamentblog.tables.prefix').'post', function (Blueprint $table) { 44 | $table->id(); 45 | $table->foreignId( "post_id") 46 | ->constrained(table: config('filamentblog.tables.prefix').'posts') 47 | ->cascadeOnDelete(); 48 | $table->foreignId("category_id") 49 | ->constrained(table: config('filamentblog.tables.prefix').'categories') 50 | ->cascadeOnDelete(); 51 | $table->timestamps(); 52 | }); 53 | 54 | Schema::create(config('filamentblog.tables.prefix').'seo_details', function (Blueprint $table) { 55 | $table->id(); 56 | $table->foreignId("post_id") 57 | ->constrained(table: config('filamentblog.tables.prefix').'posts') 58 | ->cascadeOnDelete(); 59 | $table->string('title'); 60 | $table->json('keywords')->nullable(); 61 | $table->text('description'); 62 | $table->timestamps(); 63 | }); 64 | 65 | Schema::create(config('filamentblog.tables.prefix').'comments', function (Blueprint $table) { 66 | $table->id(); 67 | $table->foreignId(config('filamentblog.user.foreign_key')); 68 | $table->foreignId("post_id") 69 | ->constrained(table: config('filamentblog.tables.prefix').'posts') 70 | ->cascadeOnDelete(); 71 | $table->text('comment'); 72 | $table->boolean('approved')->default(false); 73 | $table->dateTime('approved_at')->nullable(); 74 | $table->timestamps(); 75 | }); 76 | 77 | Schema::create(config('filamentblog.tables.prefix').'news_letters', function (Blueprint $table) { 78 | $table->id(); 79 | $table->string('email', 100)->unique(); 80 | $table->boolean('subscribed')->default(true); 81 | $table->timestamps(); 82 | }); 83 | 84 | Schema::create(config('filamentblog.tables.prefix').'tags', function (Blueprint $table) { 85 | $table->id(); 86 | $table->string('name', 50)->unique(); 87 | $table->string('slug', 155)->unique(); 88 | $table->timestamps(); 89 | }); 90 | 91 | Schema::create(config('filamentblog.tables.prefix').'post_'.config('filamentblog.tables.prefix').'tag', function (Blueprint $table) { 92 | $table->id(); 93 | $table->foreignId("post_id") 94 | ->constrained(table: config('filamentblog.tables.prefix').'posts') 95 | ->cascadeOnDelete(); 96 | $table->foreignId("tag_id") 97 | ->constrained(table: config('filamentblog.tables.prefix').'tags') 98 | ->cascadeOnDelete(); 99 | $table->timestamps(); 100 | }); 101 | 102 | 103 | // Check if the column exists 104 | if (!Schema::hasColumn($tableName, $columnName)) { 105 | // Column doesn't exist, so add it to the table 106 | Schema::table($tableName, function (Blueprint $table) use ($columnName) { 107 | $table->string($columnName)->nullable(); 108 | }); 109 | } 110 | 111 | Schema::create(config('filamentblog.tables.prefix').'share_snippets', function (Blueprint $table) { 112 | $table->id(); 113 | $table->longText('script_code'); 114 | $table->text('html_code'); 115 | $table->boolean('active')->default(true); 116 | $table->timestamps(); 117 | }); 118 | 119 | Schema::create(config('filamentblog.tables.prefix').'settings', function (Blueprint $table) { 120 | $table->id(); 121 | $table->string('title', 155)->nullable(); 122 | $table->text('description')->nullable(); 123 | $table->string('logo')->nullable(); 124 | $table->string('favicon')->nullable(); 125 | $table->string('organization_name')->nullable(); 126 | $table->tinyText('google_console_code')->nullable(); 127 | $table->text('google_analytic_code')->nullable(); 128 | $table->tinyText('google_adsense_code')->nullable(); 129 | $table->json('quick_links')->nullable(); 130 | $table->timestamps(); 131 | }); 132 | } 133 | 134 | /** 135 | * Reverse the migrations. 136 | * 137 | * @return void 138 | */ 139 | public function down() 140 | { 141 | Schema::dropIfExists(config('filamentblog.tables.prefix').'categories'); 142 | Schema::dropIfExists(config('filamentblog.user.model')); 143 | Schema::dropIfExists(config('filamentblog.tables.prefix').'posts'); 144 | Schema::dropIfExists(config('filamentblog.tables.prefix').'category_'.config('filamentblog.tables.prefix').'post'); 145 | Schema::dropIfExists(config('filamentblog.tables.prefix').'seo_details'); 146 | Schema::dropIfExists(config('filamentblog.tables.prefix').'comments'); 147 | Schema::dropIfExists(config('filamentblog.tables.prefix').'news_letters'); 148 | Schema::dropIfExists(config('filamentblog.tables.prefix').'tags'); 149 | Schema::dropIfExists(config('filamentblog.tables.prefix').'post_'.config('filamentblog.tables.prefix').'tag'); 150 | Schema::dropIfExists(config('filamentblog.tables.prefix').'share_snippets'); 151 | Schema::dropIfExists(config('filamentblog.tables.prefix').'settings'); 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /images/landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefireflytech/filament-blog/5a60ee57dd2785f8eb84a7a184240e229b59640d/images/landing.png -------------------------------------------------------------------------------- /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 | 34 | -------------------------------------------------------------------------------- /resources/views/blogs/all-post.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

5 | Latest News / Blogs 6 |

7 |
8 |
9 |
10 |
11 |
12 | @forelse ($posts as $post) 13 | 14 | @empty 15 |
16 |
17 |

No posts found

18 |
19 |
20 | @endforelse 21 |
22 |
23 | {{ $posts->links() }} 24 |
25 |
26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /resources/views/blogs/category-post.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

5 | Category: {{ $category->name }} 6 |

7 |
8 |
9 |
10 |
11 |
12 | @forelse ($posts as $post) 13 | 14 | @empty 15 |
16 |
17 |

No posts found

18 |
19 |
20 | @endforelse 21 |
22 |
23 | {{ $posts->links() }} 24 |
25 |
26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /resources/views/blogs/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | @if(count($posts)) 3 |
4 |
5 |
6 | {{-- Hero Post --}} 7 | @foreach ($posts->take(1) as $post) 8 |
9 | 10 |
11 | @endforeach 12 | {{-- Hero Post --}} 13 |
14 |
15 |
16 |
17 |
18 |
19 | @foreach ($posts->skip(1) as $post) 20 | 21 | @endforeach 22 |
23 | 31 |
32 |
33 | @else 34 |
35 |
36 |

No posts found

37 |
38 |
39 | @endif 40 | 41 |
42 | -------------------------------------------------------------------------------- /resources/views/blogs/search.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

5 | Search Results 6 |

7 |
8 |
9 |
10 |
11 |
12 | @forelse ($posts as $post) 13 | 14 | @empty 15 |
16 |

No posts found

17 |
18 | @endforelse 19 |
20 |
21 | {{ $posts->links() }} 22 |
23 |
24 |
25 | 26 |
-------------------------------------------------------------------------------- /resources/views/blogs/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | Home 6 | / 7 | Blog 8 | / 9 | 10 | {{ $post->title }} 11 | 12 |
13 |
14 |
15 |
16 |
17 | 25 |
26 | {!! $shareButton?->html_code !!} 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {{ $post->photo_alt_text }} 35 |
36 |
37 |

38 | {{ $post->title }} 39 |

40 |

{{ $post->sub_title }}

41 |
42 | @foreach ($post->categories as $category) 43 | 44 | {{ $category->name }} 45 | 46 | 47 | @endforeach 48 |
49 |
50 |
51 |
52 |
53 | {{ $post->user->name() }} 54 |
55 | {{ $post->user->name() }} 56 | 57 | {{ $post->formattedPublishedDate() }} 58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 | {!! tiptap_converter()->asHTML($post->body, toc: true, maxDepth: 3) !!} 66 |
67 | 68 | @if($post->tags->count()) 69 |
70 | Tags 71 |
72 | @foreach ($post->tags as $tag) 73 | 74 | {{ $tag->name }} 75 | 76 | @endforeach 77 |
78 |
79 | @endif 80 |
81 |
82 |
83 | @if($post->comments->count()) 84 |
85 |
86 |

Comments

87 |
88 |
89 | @foreach($post->comments as $comment) 90 |
91 |
92 | avatar 93 |
94 | 95 | 96 | {{ $comment->user->{config('filamentblog.user.columns.name')} }} 97 | 98 | 99 | {{ $comment->created_at->diffForHumans() }} 100 | 101 |
102 |
103 |

104 | {{ $comment->comment }} 105 |

106 |
107 | @endforeach 108 |
109 |
110 | @endif 111 | 112 |
113 |
114 | {{-- Ads Section --}} 115 | {{-- --}} 117 | {{-- ADS--}} 118 | {{--
--}} 119 |
120 |
121 |
122 | 123 |
124 |
125 |
126 |

127 | # Related Posts 128 |

129 |
130 | 131 |
132 |
133 |
134 | @forelse($post->relatedPosts() as $post) 135 | 136 | @empty 137 |
138 |

No related posts found.

139 |
140 | @endforelse 141 |
142 | 150 |
151 |
152 | 153 |
154 | {!! $shareButton?->script_code !!} 155 |
156 | -------------------------------------------------------------------------------- /resources/views/blogs/tag-post.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

5 | Tag: {{ $tag->name }} 6 |

7 |
8 |
9 |
10 |
11 |
12 | @forelse ($posts as $post) 13 | 14 | @empty 15 |
16 |

No posts found

17 |
18 | @endforelse 19 |
20 |
21 | {{ $posts->links() }} 22 |
23 |
24 |
25 | 26 |
-------------------------------------------------------------------------------- /resources/views/components/card.blade.php: -------------------------------------------------------------------------------- 1 | @props(['post']) 2 | 3 |
4 |
5 | {{ $post->photo_alt_text }} 7 |
8 |
9 |
10 |

12 | {{ $post->title }} 13 |

14 |

15 | {{ Str::limit($post->sub_title, 100) }} 16 |

17 |
18 |
19 | {{ $post->user->name() }} 21 |
22 | {{ $post->user->name() }} 24 | 26 | {{ $post->formattedPublishedDate() }} 27 |
28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /resources/views/components/comment.blade.php: -------------------------------------------------------------------------------- 1 | @props(['post']) 2 |
3 | @csrf 4 |
5 |
6 |

Leave a reply

7 |
8 |
9 | 10 | 12 | @error('comment') 13 |

{{ $message }}

14 | @enderror 15 | @if (session('success')) 16 | {{ session('success') }} 17 | @endif 18 |
19 |
20 | @if(auth()->user()?->canComment()) 21 | 30 | @else 31 | 34 | Login 35 | 36 | @endif 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /resources/views/components/feature-card.blade.php: -------------------------------------------------------------------------------- 1 | @props(['post']) 2 |
3 |
4 | {{ $post->photo_alt_text }} 5 |
6 |
7 |
8 |
9 | 10 | {{ $post->title }} 11 | 12 |
13 | @foreach ($post->categories as $category) 14 | 15 | {{ $category->name }} 16 | 17 | 18 | @endforeach 19 |
20 |
21 |

22 | {!! Str::limit($post->sub_title) !!} 23 |

24 |
25 |
26 | {{ $post->user->name() }} 27 |
28 | {{ $post->user->name() }} 29 | 30 | {{ $post->formattedPublishedDate() }} 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /resources/views/components/header-category.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | @foreach($categories as $category) 5 | 8 | {{ $category->name }} 9 | 10 | @endforeach 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /resources/views/components/header.blade.php: -------------------------------------------------------------------------------- 1 | @props(['title' =>'Firefly Blog', 'logo' => null] ) 2 |
3 |
4 |
5 |
6 |
7 | 8 | @if($logo) 9 | {{ $title }} 10 | @else 11 | 12 | {{ $title ?: 'Firefly Blog' }} 13 | 14 | @endif 15 | 16 | 32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | @error('query') 46 | {{ $message }} 47 | @enderror 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /resources/views/components/recent-post.blade.php: -------------------------------------------------------------------------------- 1 | @foreach ($posts as $post) 2 | 3 |

4 | {{ $post->title }} 5 |

6 |
7 | @endforeach 8 | -------------------------------------------------------------------------------- /resources/views/mails/blog-published.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | New Blog Post Notification 7 | 71 | 72 | 73 |
74 |
75 | {{ config('app.name') }} 76 |
77 |

New Blog Post Published!

78 |
79 | Feature Image 80 |
81 |
82 |

{{ $post->title }}

83 |

{!! Str::limit($post->body, 500) !!}

84 | Read More 85 |
86 | 89 |
90 | 91 | 92 | -------------------------------------------------------------------------------- /resources/views/tables/columns/user-photo-name.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @php 3 | $record = $getState(); 4 | @endphp 5 | 6 |
7 | {{ $record->{config('filamentblog.user.columns.name')} }} 8 |

{{ $record->name }}

9 |
10 |
11 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | prefix(config('filamentblog.route.prefix')) 11 | ->group(function () { 12 | Route::get('/', [PostController::class, 'index'])->name('filamentblog.post.index'); 13 | Route::get('/all', [PostController::class, 'allPosts'])->name('filamentblog.post.all'); 14 | Route::get('/search', [PostController::class, 'search'])->name('filamentblog.post.search'); 15 | Route::get('/{post:slug}', [PostController::class, 'show'])->name('filamentblog.post.show'); 16 | Route::post('/subscribe', [PostController::class, 'subscribe']) 17 | ->middleware('throttle:5,1') 18 | ->name('filamentblog.post.subscribe'); 19 | 20 | Route::get('/categories/{category:slug}', [CategoryController::class, 'posts'])->name('filamentblog.category.post'); 21 | Route::get('/tags/{tag:slug}', [TagController::class, 'posts'])->name('filamentblog.tag.post'); 22 | 23 | Route::post('/posts/{post:slug}/comment', [CommentController::class, 'store'])->middleware('auth')->name('filamentblog.comment.store'); 24 | 25 | Route::get('/login', function () { 26 | redirect(\route(config('filamentblog.route.login.name'))); 27 | })->name('filamentblog.post.login'); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /src/Blog.php: -------------------------------------------------------------------------------- 1 | resources([ 23 | Resources\CategoryResource::class, 24 | Resources\PostResource::class, 25 | Resources\TagResource::class, 26 | Resources\SeoDetailResource::class, 27 | Resources\NewsletterResource::class, 28 | Resources\CommentResource::class, 29 | Resources\ShareSnippetResource::class, 30 | Resources\SettingResource::class, 31 | ]); 32 | } 33 | 34 | public function boot(Panel $panel): void 35 | { 36 | // TODO: Implement boot() method. 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Components/Card.php: -------------------------------------------------------------------------------- 1 | \Firefly\FilamentBlog\Models\Category::all(), 13 | ]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Components/Layout.php: -------------------------------------------------------------------------------- 1 | $setting]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Components/RecentPost.php: -------------------------------------------------------------------------------- 1 | published()->whereNot('slug', request('post')->slug)->latest()->take(5)->get(); 16 | 17 | return view('filament-blog::components.recent-post', [ 18 | 'posts' => $posts, 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Concerns/HasCategories.php: -------------------------------------------------------------------------------- 1 | categoriesRelation; 14 | } 15 | 16 | public function categoriesRelation(): BelongsToMany 17 | { 18 | return $this->belongsToMany(Category::class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Console/Commands/RenameTablesCommand.php: -------------------------------------------------------------------------------- 1 | 'vendor/firefly/filament-blog/database/migrations/2024_05_11_152936_create_add_prefix_on_all_blog_tables.php', 31 | '--force' => true, 32 | ]); 33 | $this->info('Tables have been renamed successfully.'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Enums/PostStatus.php: -------------------------------------------------------------------------------- 1 | 'info', 19 | self::SCHEDULED => 'warning', 20 | self::PUBLISHED => 'success' 21 | }; 22 | } 23 | 24 | public function getLabel(): string 25 | { 26 | return match ($this) { 27 | self::PENDING => 'Pending', 28 | self::SCHEDULED => 'Scheduled', 29 | self::PUBLISHED => 'Published' 30 | }; 31 | } 32 | 33 | public function getIcon(): ?string 34 | { 35 | return match ($this) { 36 | self::PENDING => 'heroicon-o-clock', 37 | self::SCHEDULED => 'heroicon-o-calendar-days', 38 | self::PUBLISHED => 'heroicon-o-check-badge', 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 13 | SendBlogPublishedNotification::class, 14 | ], 15 | ]; 16 | 17 | /** 18 | * Register any events for your application. 19 | * 20 | * @return void 21 | */ 22 | public function boot() 23 | { 24 | parent::boot(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Events/BlogPublished.php: -------------------------------------------------------------------------------- 1 | post = $post; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Exceptions/CannotSendEmail.php: -------------------------------------------------------------------------------- 1 | name('filament-blog') 24 | ->hasConfigFile(['filamentblog']) 25 | ->hasMigrations('create_blog_tables') 26 | ->hasCommands(RenameTablesCommand::class) 27 | ->runsMigrations() 28 | ->hasViewComponents( 29 | 'blog', 30 | Layout::class, 31 | RecentPost::class, 32 | Header::class, 33 | Comment::class, 34 | HeaderCategory::class, 35 | FeatureCard::class, 36 | Card::class 37 | ) 38 | ->hasViews('filament-blog') 39 | ->hasRoute('web') 40 | ->hasInstallCommand(function (InstallCommand $installCommand) { 41 | $installCommand 42 | ->startWith(function (InstallCommand $command) { 43 | $command->info('Hello, and welcome to my great new package!'); 44 | $command->newLine(1); 45 | }) 46 | ->publishConfigFile() 47 | ->publishMigrations() 48 | ->endWith(function (InstallCommand $installCommand) { 49 | $installCommand->newLine(1); 50 | $installCommand->info('========================================================================================================'); 51 | $installCommand->info("Get ready to breathe easy! Our package has just saved you from a day's worth of headaches and hassle."); 52 | $installCommand->info('========================================================================================================'); 53 | 54 | }); 55 | }); 56 | // $this->loadTestingMigration(); 57 | } 58 | 59 | public function register() 60 | { 61 | Route::bind('post', function ($value) { 62 | return \Firefly\FilamentBlog\Models\Post::where('slug', $value)->published()->firstOrFail(); 63 | }); 64 | 65 | $this->app->register(EventServiceProvider::class); 66 | 67 | $this->app->singleton('seometa', function ($app) { 68 | return new SEOMeta(new Config($app->config->get('filamentblog.seo.meta'))); 69 | }); 70 | 71 | return parent::register(); // TODO: Change the autogenerated stub 72 | } 73 | 74 | public function loadTestingMigration(): void 75 | { 76 | if ($this->app->environment('testing')) { 77 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Http/Controllers/CategoryController.php: -------------------------------------------------------------------------------- 1 | load(['posts.user', 'posts.categories']) 13 | ->posts() 14 | ->published() 15 | ->paginate(25); 16 | 17 | return view('filament-blog::blogs.category-post', [ 18 | 'posts' => $posts, 19 | 'category' => $category, 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Controllers/CommentController.php: -------------------------------------------------------------------------------- 1 | validate([ 13 | 'comment' => 'required|min:3|max:500', 14 | ]); 15 | 16 | $post->comments()->create([ 17 | 'comment' => $request->comment, 18 | 'user_id' => $request->user()->id, 19 | 'approved' => false, 20 | ]); 21 | 22 | return redirect() 23 | ->route('filamentblog.post.show', $post) 24 | ->with('success', 'Comment submitted for approval.'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | with(['categories', 'user', 'tags']) 19 | ->published() 20 | ->paginate(10); 21 | 22 | return view('filament-blog::blogs.index', [ 23 | 'posts' => $posts, 24 | ]); 25 | } 26 | 27 | public function allPosts() 28 | { 29 | SEOMeta::setTitle('All posts | '.config('app.name')) ; 30 | 31 | $posts = Post::query()->with(['categories', 'user']) 32 | ->published() 33 | ->paginate(20); 34 | 35 | return view('filament-blog::blogs.all-post', [ 36 | 'posts' => $posts, 37 | ]); 38 | } 39 | 40 | public function search(Request $request) 41 | { 42 | SEOMeta::setTitle('Search result for '.$request->get('query')); 43 | 44 | $request->validate([ 45 | 'query' => 'required', 46 | ]); 47 | $searchedPosts = Post::query() 48 | ->with(['categories', 'user']) 49 | ->published() 50 | ->whereAny(['title', 'sub_title'], 'like', '%'.$request->get('query').'%') 51 | ->paginate(10)->withQueryString(); 52 | 53 | return view('filament-blog::blogs.search', [ 54 | 'posts' => $searchedPosts, 55 | 'searchMessage' => 'Search result for '.$request->get('query'), 56 | ]); 57 | } 58 | 59 | public function show(Post $post) 60 | { 61 | SEOMeta::setTitle($post->seoDetail?->title); 62 | 63 | SEOMeta::setDescription($post->seoDetail?->description); 64 | 65 | SEOMeta::setKeywords($post->seoDetail->keywords ?? []); 66 | 67 | $shareButton = ShareSnippet::query()->active()->first(); 68 | $post->load(['user', 'categories', 'tags', 'comments' => fn ($query) => $query->approved(), 'comments.user']); 69 | 70 | return view('filament-blog::blogs.show', [ 71 | 'post' => $post, 72 | 'shareButton' => $shareButton, 73 | ]); 74 | } 75 | 76 | public function subscribe(Request $request) 77 | { 78 | $request->validate([ 79 | 'email' => [ 80 | 'required', 81 | 'email', 82 | Rule::unique(NewsLetter::class, 'email') 83 | ], 84 | ], [ 85 | 'email.unique' => 'You have already subscribed', 86 | ]); 87 | 88 | NewsLetter::create([ 89 | 'email' => $request->email, 90 | ]); 91 | 92 | return back()->with('success', 'You have successfully subscribed to our news letter'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Http/Controllers/TagController.php: -------------------------------------------------------------------------------- 1 | load(['posts.user']) 13 | ->posts() 14 | ->published() 15 | ->paginate(25); 16 | 17 | return view('filament-blog::blogs.tag-post', [ 18 | 'posts' => $posts, 19 | 'tag' => $tag, 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Jobs/PostScheduleJob.php: -------------------------------------------------------------------------------- 1 | post->update([ 27 | 'status' => PostStatus::PUBLISHED, 28 | 'published_at' => now(), 29 | 'scheduled_for' => null, 30 | ]); 31 | Log::info('PostScheduleJob Ended'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Listeners/SendBlogPublishedNotification.php: -------------------------------------------------------------------------------- 1 | get(); 14 | 15 | foreach ($subscribers as $subscriber) { 16 | Mail::queue(new BlogPublished($event->post, $subscriber->email)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Mails/BlogPublished.php: -------------------------------------------------------------------------------- 1 | post->isNotPublished()) { 28 | throw CannotSendEmail::postNotPublished(); 29 | } 30 | 31 | return new Envelope( 32 | to: $this->toEamil, 33 | subject: 'New Purchase Mail' 34 | ); 35 | 36 | } 37 | 38 | public function content(): Content 39 | { 40 | return new Content( 41 | view: 'filament-blog::mails.blog-published', 42 | with: ['post' => $this->post]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Models/Category.php: -------------------------------------------------------------------------------- 1 | 'integer', 25 | ]; 26 | 27 | public function posts(): BelongsToMany 28 | { 29 | return $this->belongsToMany(Post::class, config('filamentblog.tables.prefix').'category_'.config('filamentblog.tables.prefix').'post'); 30 | } 31 | 32 | public static function getForm() 33 | { 34 | return [ 35 | TextInput::make('name') 36 | ->live(true) 37 | ->afterStateUpdated(function (Get $get, Set $set, ?string $operation, ?string $old, ?string $state) { 38 | 39 | $set('slug', Str::slug($state)); 40 | }) 41 | ->unique(config('filamentblog.tables.prefix').'categories', 'name', null, 'id') 42 | ->required() 43 | ->maxLength(155), 44 | 45 | TextInput::make('slug') 46 | ->unique(config('filamentblog.tables.prefix').'categories', 'slug', null, 'id') 47 | ->readOnly() 48 | ->maxLength(255), 49 | ]; 50 | } 51 | 52 | protected static function newFactory() 53 | { 54 | return new CategoryFactory(); 55 | } 56 | 57 | public function getTable() 58 | { 59 | return config('filamentblog.tables.prefix') . 'categories'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Models/CategoryPost.php: -------------------------------------------------------------------------------- 1 | 'integer', 30 | 'post_id' => 'integer', 31 | 'category_id' => 'integer', 32 | ]; 33 | 34 | protected static function newFactory() 35 | { 36 | return new CategoryPostFactory(); 37 | } 38 | 39 | public function getTable() 40 | { 41 | return config('filamentblog.tables.prefix') . 'category_' . config('filamentblog.tables.prefix') . 'post'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Models/Comment.php: -------------------------------------------------------------------------------- 1 | 'integer', 38 | 'user_id' => 'integer', 39 | 'post_id' => 'integer', 40 | 'approved' => 'boolean', 41 | 'approved_at' => 'datetime', 42 | ]; 43 | 44 | public function user(): BelongsTo 45 | { 46 | return $this->belongsTo(config('filamentblog.user.model'), 'user_id'); 47 | } 48 | 49 | public function post(): BelongsTo 50 | { 51 | return $this->belongsTo(Post::class); 52 | } 53 | 54 | protected static function newFactory() 55 | { 56 | return new CommentFactory(); 57 | } 58 | 59 | public function scopeApproved(Builder $query) 60 | { 61 | return $query->where('approved', true); 62 | } 63 | 64 | public static function getForm(): array 65 | { 66 | return [ 67 | Select::make('user_id') 68 | ->relationship('user', config('filamentblog.user.columns.name')) 69 | ->required(), 70 | Select::make('post_id') 71 | ->relationship('post', 'title') 72 | ->required(), 73 | Textarea::make('comment') 74 | ->required() 75 | ->maxLength(65535) 76 | ->columnSpanFull(), 77 | Toggle::make('approved'), 78 | ]; 79 | } 80 | 81 | public function getTable() 82 | { 83 | return config('filamentblog.tables.prefix') . 'comments'; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Models/NewsLetter.php: -------------------------------------------------------------------------------- 1 | 'integer', 30 | 'active' => 'boolean', 31 | ]; 32 | 33 | public function scopeSubscribed() 34 | { 35 | return $this->where('subscribed', true); 36 | } 37 | 38 | protected static function newFactory() 39 | { 40 | return new NewsLetterFactory(); 41 | } 42 | 43 | public function getTable() 44 | { 45 | return config('filamentblog.tables.prefix') . 'news_letters'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Models/Post.php: -------------------------------------------------------------------------------- 1 | 'integer', 54 | 'published_at' => 'datetime', 55 | 'scheduled_for' => 'datetime', 56 | 'status' => PostStatus::class, 57 | 'user_id' => 'integer', 58 | ]; 59 | 60 | protected static function newFactory() 61 | { 62 | return new PostFactory(); 63 | } 64 | 65 | public function categories() 66 | { 67 | return $this->belongsToMany(Category::class, config('filamentblog.tables.prefix').'category_'.config('filamentblog.tables.prefix').'post'); 68 | } 69 | 70 | public function comments(): hasmany 71 | { 72 | return $this->hasMany(Comment::class); 73 | } 74 | 75 | public function tags(): BelongsToMany 76 | { 77 | return $this->belongsToMany(Tag::class,config('filamentblog.tables.prefix').'post_'.config('filamentblog.tables.prefix').'tag'); 78 | } 79 | 80 | public function user(): BelongsTo 81 | { 82 | return $this->belongsTo(config('filamentblog.user.model'), config('filamentblog.user.foreign_key')); 83 | } 84 | 85 | public function seoDetail() 86 | { 87 | return $this->hasOne(SeoDetail::class); 88 | } 89 | 90 | public function isNotPublished() 91 | { 92 | return ! $this->isStatusPublished(); 93 | } 94 | 95 | public function scopePublished(Builder $query) 96 | { 97 | return $query->where('status', PostStatus::PUBLISHED)->latest('published_at'); 98 | } 99 | 100 | public function scopeScheduled(Builder $query) 101 | { 102 | return $query->where('status', PostStatus::SCHEDULED)->latest('scheduled_for'); 103 | } 104 | 105 | public function scopePending(Builder $query) 106 | { 107 | return $query->where('status', PostStatus::PENDING)->latest('created_at'); 108 | } 109 | 110 | public function formattedPublishedDate() 111 | { 112 | return $this->published_at?->format('d M Y'); 113 | } 114 | 115 | public function isScheduled() 116 | { 117 | return $this->status === PostStatus::SCHEDULED; 118 | } 119 | 120 | public function isStatusPublished() 121 | { 122 | return $this->status === PostStatus::PUBLISHED; 123 | } 124 | 125 | public function relatedPosts($take = 3) 126 | { 127 | return $this->whereHas('categories', function ($query) { 128 | $query->whereIn(config('filamentblog.tables.prefix').'categories.id', $this->categories->pluck('id')) 129 | ->whereNotIn(config('filamentblog.tables.prefix').'posts.id', [$this->id]); 130 | })->published()->with('user')->take($take)->get(); 131 | } 132 | 133 | protected function getFeaturePhotoAttribute() 134 | { 135 | return asset('storage/'.$this->cover_photo_path); 136 | } 137 | 138 | public static function getForm() 139 | { 140 | return [ 141 | Section::make('Blog Details') 142 | ->schema([ 143 | Fieldset::make('Titles') 144 | ->schema([ 145 | Select::make('category_id') 146 | ->multiple() 147 | ->preload() 148 | ->createOptionForm(Category::getForm()) 149 | ->searchable() 150 | ->relationship('categories', 'name') 151 | ->columnSpanFull(), 152 | 153 | TextInput::make('title') 154 | ->live(true) 155 | ->afterStateUpdated(fn (Set $set, ?string $state) => $set( 156 | 'slug', 157 | Str::slug($state) 158 | )) 159 | ->required() 160 | ->unique(config('filamentblog.tables.prefix').'posts', 'title', null, 'id') 161 | ->maxLength(255), 162 | 163 | TextInput::make('slug') 164 | ->maxLength(255), 165 | 166 | Textarea::make('sub_title') 167 | ->maxLength(255) 168 | ->columnSpanFull(), 169 | 170 | Select::make('tag_id') 171 | ->multiple() 172 | ->preload() 173 | ->createOptionForm(Tag::getForm()) 174 | ->searchable() 175 | ->relationship('tags', 'name') 176 | ->columnSpanFull(), 177 | ]), 178 | TiptapEditor::make('body') 179 | ->profile('default') 180 | ->disableFloatingMenus() 181 | ->extraInputAttributes(['style' => 'max-height: 30rem; min-height: 24rem']) 182 | ->required() 183 | ->columnSpanFull(), 184 | Fieldset::make('Feature Image') 185 | ->schema([ 186 | FileUpload::make('cover_photo_path') 187 | ->label('Cover Photo') 188 | ->directory('/blog-feature-images') 189 | ->hint('This cover image is used in your blog post as a feature image. Recommended image size 1200 X 628') 190 | ->image() 191 | ->preserveFilenames() 192 | ->imageEditor() 193 | ->maxSize(1024 * 5) 194 | ->rules('dimensions:max_width=1920,max_height=1004') 195 | ->required(), 196 | TextInput::make('photo_alt_text')->required(), 197 | ])->columns(1), 198 | 199 | Fieldset::make('Status') 200 | ->schema([ 201 | 202 | ToggleButtons::make('status') 203 | ->live() 204 | ->inline() 205 | ->options(PostStatus::class) 206 | ->required(), 207 | 208 | DateTimePicker::make('scheduled_for') 209 | ->visible(function ($get) { 210 | return $get('status') === PostStatus::SCHEDULED->value; 211 | }) 212 | ->required(function ($get) { 213 | return $get('status') === PostStatus::SCHEDULED->value; 214 | }) 215 | ->minDate(now()->addMinutes(5)) 216 | ->native(false), 217 | ]), 218 | Select::make(config('filamentblog.user.foreign_key')) 219 | ->relationship('user', config('filamentblog.user.columns.name')) 220 | ->nullable(false) 221 | ->default(auth()->id()), 222 | 223 | ]), 224 | ]; 225 | } 226 | 227 | public function getTable() 228 | { 229 | return config('filamentblog.tables.prefix') . 'posts'; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Models/PostTag.php: -------------------------------------------------------------------------------- 1 | 'integer', 32 | 'post_id' => 'integer', 33 | 'tag_id' => 'integer', 34 | ]; 35 | 36 | protected static function newFactory() 37 | { 38 | return new PostTagFactory(); 39 | } 40 | 41 | public function getTable() 42 | { 43 | return config('filamentblog.tables.prefix') . 'post_' . config('filamentblog.tables.prefix') . 'tag'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Models/SeoDetail.php: -------------------------------------------------------------------------------- 1 | 'integer', 59 | 'post_id' => 'integer', 60 | 'user_id' => 'integer', 61 | 'keywords' => 'json', 62 | ]; 63 | 64 | public function post(): BelongsTo 65 | { 66 | return $this->belongsTo(Post::class)->orderByDesc('id'); 67 | } 68 | 69 | public static function getForm() 70 | { 71 | return [ 72 | Select::make('post_id') 73 | ->createOptionForm(Post::getForm()) 74 | ->editOptionForm(Post::getForm()) 75 | ->relationship('post', 'title') 76 | ->unique(config('filamentblog.tables.prefix').'seo_details', 'post_id', null, 'id') 77 | ->required() 78 | ->preload() 79 | ->searchable() 80 | ->default(request('post_id') ?? '') 81 | ->columnSpanFull(), 82 | TextInput::make('title') 83 | ->required() 84 | ->maxLength(255) 85 | ->columnSpanFull(), 86 | TagsInput::make('keywords') 87 | ->columnSpanFull(), 88 | Textarea::make('description') 89 | ->required() 90 | ->maxLength(65535) 91 | ->columnSpanFull(), 92 | ]; 93 | } 94 | 95 | protected static function newFactory() 96 | { 97 | return new SeoDetailFactory(); 98 | } 99 | 100 | public function getTable() 101 | { 102 | return config('filamentblog.tables.prefix') . 'seo_details'; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Models/Setting.php: -------------------------------------------------------------------------------- 1 | 'json', 35 | 'created_at' => 'datetime', 36 | 'updated_at' => 'datetime', 37 | ]; 38 | 39 | protected function getLogoImageAttribute() 40 | { 41 | return asset('storage/' . $this->logo); 42 | } 43 | 44 | protected function getFavIconImageAttribute() 45 | { 46 | return asset('storage/' . $this->favicon); 47 | } 48 | 49 | protected static function newFactory() 50 | { 51 | return new SettingFactory(); 52 | } 53 | 54 | public static function getForm(): array 55 | { 56 | return [ 57 | Section::make('General Information') 58 | ->schema([ 59 | TextInput::make('title') 60 | ->maxLength(155) 61 | ->required(), 62 | TextInput::make('organization_name') 63 | ->required() 64 | ->maxLength(155) 65 | ->minLength(3), 66 | Textarea::make('description') 67 | ->required() 68 | ->minLength(10) 69 | ->maxLength(1000) 70 | ->columnSpanFull(), 71 | FileUpload::make('logo') 72 | ->hint('Max height 400') 73 | ->directory('setting/logo') 74 | ->maxSize(1024 * 1024 * 2) 75 | ->rules('dimensions:max_height=400') 76 | ->nullable()->columnSpanFull(), 77 | FileUpload::make('favicon') 78 | ->directory('setting/favicon') 79 | ->maxSize(50 ) 80 | ->nullable()->columnSpanFull() 81 | ])->columns(2), 82 | 83 | Section::make('SEO') 84 | ->description('Place your google analytic and adsense code here. This will be added to the head tag of your blog post only.') 85 | ->schema([ 86 | Textarea::make('google_console_code') 87 | ->startsWith('nullable() 89 | ->columnSpanFull(), 90 | Textarea::make('google_analytic_code') 91 | ->startsWith('endsWith('') 93 | ->nullable() 94 | ->columnSpanFull(), 95 | Textarea::make('google_adsense_code') 96 | ->startsWith('endsWith('') 98 | ->nullable() 99 | ->columnSpanFull(), 100 | ])->columns(2), 101 | Section::make('Quick Links') 102 | ->description('Add your quick links here. This will be displayed in the footer of your blog.') 103 | ->schema([ 104 | Repeater::make('quick_links') 105 | ->label('Links') 106 | ->schema([ 107 | TextInput::make('label') 108 | ->required() 109 | ->maxLength(155), 110 | TextInput::make('url') 111 | ->label('URL') 112 | ->helperText('URL should start with http:// or https://') 113 | ->required() 114 | ->url() 115 | ->maxLength(255), 116 | ])->columns(2), 117 | ])->columnSpanFull(), 118 | ]; 119 | } 120 | 121 | public function getTable() 122 | { 123 | return config('filamentblog.tables.prefix') . 'settings'; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Models/ShareSnippet.php: -------------------------------------------------------------------------------- 1 | 'string', 28 | 'html_code' => 'string', 29 | ]; 30 | 31 | public function scopeActive(Builder $query) 32 | { 33 | return $query->where('active', true); 34 | } 35 | 36 | public static function getForm(): array 37 | { 38 | return [ 39 | Textarea::make('script_code') 40 | ->label('JS Script') 41 | ->required(), 42 | Textarea::make('html_code') 43 | ->required(), 44 | Toggle::make('active'), 45 | ]; 46 | } 47 | 48 | protected static function newFactory() 49 | { 50 | return new ShareSnippetFactory(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Models/Tag.php: -------------------------------------------------------------------------------- 1 | 'integer', 24 | ]; 25 | 26 | public function posts(): BelongsToMany 27 | { 28 | 29 | return $this->belongsToMany(Post::class, config('filamentblog.tables.prefix').'post_'.config('filamentblog.tables.prefix').'tag'); 30 | } 31 | 32 | public static function getForm(): array 33 | { 34 | return [ 35 | TextInput::make('name') 36 | ->live(true)->afterStateUpdated(fn(Set $set, ?string $state) => $set( 37 | 'slug', 38 | Str::slug($state) 39 | )) 40 | ->unique(config('filamentblog.tables.prefix').'tags', 'name', null, 'id') 41 | ->required() 42 | ->maxLength(50), 43 | 44 | TextInput::make('slug') 45 | ->unique(config('filamentblog.tables.prefix').'tags', 'slug', null, 'id') 46 | ->readOnly() 47 | ->maxLength(155), 48 | ]; 49 | } 50 | 51 | protected static function newFactory() 52 | { 53 | return new TagFactory(); 54 | } 55 | 56 | public function getTable() 57 | { 58 | return config('filamentblog.tables.prefix') . 'tags'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Models/User.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected $fillable = [ 22 | 'name', 23 | 'email', 24 | 'password', 25 | ]; 26 | 27 | /** 28 | * The attributes that should be hidden for serialization. 29 | * 30 | * @var array 31 | */ 32 | protected $hidden = [ 33 | 'password', 34 | 'remember_token', 35 | ]; 36 | 37 | /** 38 | * The attributes that should be cast. 39 | * 40 | * @var array 41 | */ 42 | protected $casts = [ 43 | 'email_verified_at' => 'datetime', 44 | 'password' => 'hashed', 45 | ]; 46 | 47 | public function canComment() 48 | { 49 | return true; 50 | } 51 | 52 | public function posts() 53 | { 54 | return $this->hasMany(Post::class); 55 | } 56 | 57 | protected static function newFactory() 58 | { 59 | return new UserFactory(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Resources/CategoryResource.php: -------------------------------------------------------------------------------- 1 | schema(Category::getForm()); 29 | } 30 | 31 | public static function table(Table $table): Table 32 | { 33 | return $table 34 | ->columns([ 35 | Tables\Columns\TextColumn::make('name') 36 | ->searchable(), 37 | Tables\Columns\TextColumn::make('slug'), 38 | Tables\Columns\TextColumn::make('posts_count') 39 | ->badge() 40 | ->counts('posts'), 41 | Tables\Columns\TextColumn::make('created_at') 42 | ->dateTime() 43 | ->sortable() 44 | ->toggleable(isToggledHiddenByDefault: true), 45 | Tables\Columns\TextColumn::make('updated_at') 46 | ->dateTime() 47 | ->sortable() 48 | ->toggleable(isToggledHiddenByDefault: true), 49 | ]) 50 | ->filters([ 51 | // 52 | ]) 53 | ->actions([ 54 | Tables\Actions\EditAction::make(), 55 | Tables\Actions\ViewAction::make(), 56 | Tables\Actions\DeleteAction::make(), 57 | ]) 58 | ->bulkActions([ 59 | Tables\Actions\BulkActionGroup::make([ 60 | Tables\Actions\DeleteBulkAction::make(), 61 | ]), 62 | ]); 63 | } 64 | 65 | public static function infolist(Infolist $infolist): Infolist 66 | { 67 | return $infolist->schema([ 68 | Section::make('Category') 69 | ->schema([ 70 | TextEntry::make('name'), 71 | TextEntry::make('slug'), 72 | ])->columns(2) 73 | ->icon('heroicon-o-square-3-stack-3d'), 74 | ]); 75 | } 76 | 77 | public static function getRelations(): array 78 | { 79 | return [ 80 | PostsRelationManager::class, 81 | ]; 82 | } 83 | 84 | public static function getPages(): array 85 | { 86 | return [ 87 | 'index' => \Firefly\FilamentBlog\Resources\CategoryResource\Pages\ListCategories::route('/'), 88 | 'edit' => \Firefly\FilamentBlog\Resources\CategoryResource\Pages\EditCategory::route('/{record}/edit'), 89 | 'view' => \Firefly\FilamentBlog\Resources\CategoryResource\Pages\ViewCategory::route('/{record}'), 90 | ]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Resources/CategoryResource/Pages/CreateCategory.php: -------------------------------------------------------------------------------- 1 | slideOver() 19 | ->form(Category::getForm()), 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Resources/CategoryResource/RelationManagers/PostsRelationManager.php: -------------------------------------------------------------------------------- 1 | schema(Post::getForm()); 25 | } 26 | 27 | public function table(Table $table): Table 28 | { 29 | return $table 30 | ->recordTitleAttribute('title') 31 | ->columns([ 32 | Tables\Columns\TextColumn::make('title') 33 | ->limit(40) 34 | ->description(function (Post $record) { 35 | return Str::limit($record->sub_title); 36 | }), 37 | Tables\Columns\TextColumn::make('status') 38 | ->badge() 39 | ->color(function ($state) { 40 | return $state->getColor(); 41 | }), 42 | ]) 43 | ->filters([ 44 | // 45 | ]) 46 | ->headerActions([ 47 | Tables\Actions\CreateAction::make(), 48 | ]) 49 | ->actions([ 50 | Tables\Actions\EditAction::make()->slideOver(), 51 | Tables\Actions\DeleteAction::make(), 52 | ]) 53 | ->bulkActions([ 54 | Tables\Actions\BulkActionGroup::make([ 55 | Tables\Actions\DeleteBulkAction::make(), 56 | ]), 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Resources/CommentResource.php: -------------------------------------------------------------------------------- 1 | schema(Comment::getForm()); 27 | } 28 | 29 | public static function table(Table $table): Table 30 | { 31 | return $table 32 | ->columns([ 33 | UserPhotoName::make('user') 34 | ->label('User'), 35 | Tables\Columns\TextColumn::make('post.title') 36 | ->numeric() 37 | ->limit(20) 38 | ->sortable(), 39 | Tables\Columns\TextColumn::make('comment') 40 | ->searchable() 41 | ->limit(20), 42 | Tables\Columns\ToggleColumn::make('approved') 43 | ->beforeStateUpdated(function ($record, $state) { 44 | if ($state) { 45 | $record->approved_at = now(); 46 | } else { 47 | $record->approved_at = null; 48 | } 49 | 50 | return $state; 51 | }), 52 | Tables\Columns\TextColumn::make('approved_at') 53 | ->sortable() 54 | ->placeholder('Not approved yet'), 55 | 56 | Tables\Columns\TextColumn::make('created_at') 57 | ->dateTime() 58 | ->sortable() 59 | ->toggleable(isToggledHiddenByDefault: true), 60 | Tables\Columns\TextColumn::make('updated_at') 61 | ->dateTime() 62 | ->sortable() 63 | ->toggleable(isToggledHiddenByDefault: true), 64 | ]) 65 | ->filters([ 66 | Tables\Filters\SelectFilter::make('user') 67 | ->relationship('user', config('filamentblog.user.columns.name')) 68 | ->searchable() 69 | ->preload() 70 | ->multiple(), 71 | ]) 72 | ->actions([ 73 | ActionGroup::make([ 74 | Tables\Actions\EditAction::make(), 75 | Tables\Actions\DeleteAction::make(), 76 | Tables\Actions\ViewAction::make(), 77 | ]), 78 | ]) 79 | ->bulkActions([ 80 | Tables\Actions\BulkActionGroup::make([ 81 | Tables\Actions\DeleteBulkAction::make(), 82 | ]), 83 | ]); 84 | } 85 | 86 | public static function getRelations(): array 87 | { 88 | return [ 89 | // 90 | ]; 91 | } 92 | 93 | public static function getPages(): array 94 | { 95 | return [ 96 | 'index' => \Firefly\FilamentBlog\Resources\CommentResource\Pages\ListComments::route('/'), 97 | 'create' => \Firefly\FilamentBlog\Resources\CommentResource\Pages\CreateComment::route('/create'), 98 | 'edit' => \Firefly\FilamentBlog\Resources\CommentResource\Pages\EditComment::route('/{record}/edit'), 99 | ]; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Resources/CommentResource/Pages/CreateComment.php: -------------------------------------------------------------------------------- 1 | schema([ 26 | Forms\Components\TextInput::make('email') 27 | ->email() 28 | ->required() 29 | ->unique(ignoreRecord: true) 30 | ->maxLength(100), 31 | Forms\Components\Toggle::make('subscribed') 32 | ->default(true) 33 | ->required()->columnSpanFull(), 34 | ])->columns(2); 35 | } 36 | 37 | public static function table(Table $table): Table 38 | { 39 | return $table 40 | ->columns([ 41 | Tables\Columns\TextColumn::make('email') 42 | ->searchable(), 43 | Tables\Columns\ToggleColumn::make('subscribed') 44 | ->label('Subscribed'), 45 | Tables\Columns\TextColumn::make('created_at') 46 | ->dateTime() 47 | ->sortable() 48 | ->toggleable(isToggledHiddenByDefault: true), 49 | Tables\Columns\TextColumn::make('updated_at') 50 | ->dateTime() 51 | ->sortable() 52 | ->toggleable(isToggledHiddenByDefault: true), 53 | ]) 54 | ->filters([ 55 | // 56 | ]) 57 | ->actions([ 58 | Tables\Actions\EditAction::make(), 59 | ]) 60 | ->bulkActions([ 61 | Tables\Actions\BulkActionGroup::make([ 62 | Tables\Actions\DeleteBulkAction::make(), 63 | ]), 64 | ]); 65 | } 66 | 67 | public static function getRelations(): array 68 | { 69 | return [ 70 | // 71 | ]; 72 | } 73 | 74 | public static function getPages(): array 75 | { 76 | return [ 77 | 'index' => \Firefly\FilamentBlog\Resources\NewsletterResource\Pages\ListNewsletters::route('/'), 78 | 'create' => \Firefly\FilamentBlog\Resources\NewsletterResource\Pages\CreateNewsletter::route('/create'), 79 | 'edit' => \Firefly\FilamentBlog\Resources\NewsletterResource\Pages\EditNewsletter::route('/{record}/edit'), 80 | ]; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Resources/NewsletterResource/Pages/CreateNewsletter.php: -------------------------------------------------------------------------------- 1 | schema(Post::getForm()); 48 | } 49 | 50 | public static function table(Table $table): Table 51 | { 52 | return $table 53 | ->deferLoading() 54 | ->columns([ 55 | Tables\Columns\TextColumn::make('title') 56 | ->description(function (Post $record) { 57 | return Str::limit($record->sub_title, 40); 58 | }) 59 | ->searchable()->limit(20), 60 | Tables\Columns\TextColumn::make('status') 61 | ->badge() 62 | ->color(function ($state) { 63 | return $state->getColor(); 64 | }), 65 | Tables\Columns\ImageColumn::make('cover_photo_path')->label('Cover Photo'), 66 | 67 | UserPhotoName::make('user') 68 | ->label('Author'), 69 | 70 | Tables\Columns\TextColumn::make('created_at') 71 | ->dateTime() 72 | ->sortable() 73 | ->toggleable(isToggledHiddenByDefault: true), 74 | Tables\Columns\TextColumn::make('updated_at') 75 | ->dateTime() 76 | ->sortable() 77 | ->toggleable(isToggledHiddenByDefault: true), 78 | ])->defaultSort('id', 'desc') 79 | ->filters([ 80 | Tables\Filters\SelectFilter::make('user') 81 | ->relationship('user', config('filamentblog.user.columns.name')) 82 | ->searchable() 83 | ->preload() 84 | ->multiple(), 85 | ]) 86 | ->actions([ 87 | Tables\Actions\ActionGroup::make([ 88 | Tables\Actions\EditAction::make(), 89 | Tables\Actions\ViewAction::make(), 90 | ]), 91 | ]) 92 | ->bulkActions([ 93 | Tables\Actions\BulkActionGroup::make([ 94 | Tables\Actions\DeleteBulkAction::make(), 95 | ]), 96 | ]); 97 | } 98 | 99 | public static function infolist(Infolist $infolist): Infolist 100 | { 101 | return $infolist->schema([ 102 | Section::make('Post') 103 | ->schema([ 104 | Fieldset::make('General') 105 | ->schema([ 106 | TextEntry::make('title'), 107 | TextEntry::make('slug'), 108 | TextEntry::make('sub_title'), 109 | ]), 110 | Fieldset::make('Publish Information') 111 | ->schema([ 112 | TextEntry::make('status') 113 | ->badge()->color(function ($state) { 114 | return $state->getColor(); 115 | }), 116 | TextEntry::make('published_at')->visible(function (Post $record) { 117 | return $record->status === PostStatus::PUBLISHED; 118 | }), 119 | 120 | TextEntry::make('scheduled_for')->visible(function (Post $record) { 121 | return $record->status === PostStatus::SCHEDULED; 122 | }), 123 | ]), 124 | Fieldset::make('Description') 125 | ->schema([ 126 | TextEntry::make('body') 127 | ->html() 128 | ->columnSpanFull(), 129 | ]), 130 | ]), 131 | ]); 132 | } 133 | 134 | public static function getRecordSubNavigation(Page $page): array 135 | { 136 | return $page->generateNavigationItems([ 137 | ViewPost::class, 138 | ManaePostSeoDetail::class, 139 | ManagePostComments::class, 140 | EditPost::class, 141 | ]); 142 | } 143 | 144 | public static function getRelations(): array 145 | { 146 | return [ 147 | // \Firefly\FilamentBlog\Resources\PostResource\RelationManagers\SeoDetailRelationManager::class, 148 | // \Firefly\FilamentBlog\Resources\PostResource\RelationManagers\CommentsRelationManager::class, 149 | ]; 150 | } 151 | 152 | public static function getWidgets(): array 153 | { 154 | return [ 155 | BlogPostPublishedChart::class, 156 | ]; 157 | } 158 | 159 | public static function getPages(): array 160 | { 161 | return [ 162 | 'index' => \Firefly\FilamentBlog\Resources\PostResource\Pages\ListPosts::route('/'), 163 | 'create' => \Firefly\FilamentBlog\Resources\PostResource\Pages\CreatePost::route('/create'), 164 | 'edit' => \Firefly\FilamentBlog\Resources\PostResource\Pages\EditPost::route('/{record}/edit'), 165 | 'view' => \Firefly\FilamentBlog\Resources\PostResource\Pages\ViewPost::route('/{record}'), 166 | 'comments' => \Firefly\FilamentBlog\Resources\PostResource\Pages\ManagePostComments::route('/{record}/comments'), 167 | 'seoDetail' => \Firefly\FilamentBlog\Resources\PostResource\Pages\ManaePostSeoDetail::route('/{record}/seo-details'), 168 | ]; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Resources/PostResource/Pages/CreatePost.php: -------------------------------------------------------------------------------- 1 | record->isScheduled()) { 24 | 25 | $now = Carbon::now(); 26 | $scheduledFor = Carbon::parse($this->record->scheduled_for); 27 | PostScheduleJob::dispatch($this->record) 28 | ->delay($now->diffInSeconds($scheduledFor)); 29 | } 30 | if ($this->record->isStatusPublished()) { 31 | $this->record->published_at = date('Y-m-d H:i:s'); 32 | $this->record->save(); 33 | event(new BlogPublished($this->record)); 34 | } 35 | } 36 | 37 | protected function getRedirectUrl(): string 38 | { 39 | return SeoDetailResource::getUrl('create', ['post_id' => $this->record->id]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Resources/PostResource/Pages/EditPost.php: -------------------------------------------------------------------------------- 1 | data['status'] === PostStatus::PUBLISHED->value) { 24 | $this->record->published_at = $this->record->published_at ?? date('Y-m-d H:i:s'); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Resources/PostResource/Pages/ListPosts.php: -------------------------------------------------------------------------------- 1 | Tab::make('All'), 33 | 'published' => Tab::make('Published') 34 | ->modifyQueryUsing(function ($query) { 35 | $query->published(); 36 | })->icon('heroicon-o-check-badge'), 37 | 'pending' => Tab::make('Pending') 38 | ->modifyQueryUsing(function ($query) { 39 | $query->pending(); 40 | }) 41 | ->icon('heroicon-o-clock'), 42 | 'scheduled' => Tab::make('Scheduled') 43 | ->modifyQueryUsing(function ($query) { 44 | $query->scheduled(); 45 | }) 46 | ->icon('heroicon-o-calendar-days'), 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Resources/PostResource/Pages/ManaePostSeoDetail.php: -------------------------------------------------------------------------------- 1 | getRecordTitle(); 27 | 28 | $recordTitle = $recordTitle instanceof Htmlable ? $recordTitle->toHtml() : $recordTitle; 29 | 30 | return 'Manage Seo Detail'; 31 | } 32 | 33 | public static function getNavigationLabel(): string 34 | { 35 | return 'Manage Seo Detail'; 36 | } 37 | 38 | protected function canCreate(): bool 39 | { 40 | return ! $this->getRelationship()->count(); 41 | } 42 | 43 | public function form(Form $form): Form 44 | { 45 | return $form 46 | ->schema([ 47 | TextInput::make('title') 48 | ->required() 49 | ->maxLength(255) 50 | ->columnSpanFull(), 51 | TagsInput::make('keywords') 52 | ->columnSpanFull(), 53 | Textarea::make('description') 54 | ->required() 55 | ->maxLength(65535) 56 | ->columnSpanFull(), 57 | ]); 58 | } 59 | 60 | public function table(Table $table): Table 61 | { 62 | return $table 63 | ->recordTitleAttribute('title') 64 | ->columns([ 65 | Tables\Columns\TextColumn::make('title') 66 | ->limit(20), 67 | Tables\Columns\TextColumn::make('description') 68 | ->limit(40), 69 | Tables\Columns\TextColumn::make('keywords')->badge(), 70 | Tables\Columns\TextColumn::make('created_at') 71 | ->dateTime() 72 | ->toggleable(isToggledHiddenByDefault: true), 73 | Tables\Columns\TextColumn::make('updated_at') 74 | ->dateTime() 75 | ->toggleable(isToggledHiddenByDefault: true), 76 | ]) 77 | ->filters([ 78 | // 79 | ]) 80 | ->headerActions([ 81 | Tables\Actions\CreateAction::make(), 82 | ]) 83 | ->actions([ 84 | Tables\Actions\EditAction::make(), 85 | Tables\Actions\ViewAction::make(), 86 | ])->paginated(false); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Resources/PostResource/Pages/ManagePostComments.php: -------------------------------------------------------------------------------- 1 | getRecordTitle(); 30 | 31 | $recordTitle = $recordTitle instanceof Htmlable ? $recordTitle->toHtml() : $recordTitle; 32 | 33 | return 'Manage Comments'; 34 | } 35 | 36 | public function getBreadcrumb(): string 37 | { 38 | return 'Comments'; 39 | } 40 | 41 | public static function getNavigationLabel(): string 42 | { 43 | return 'Manage Comments'; 44 | } 45 | 46 | public function form(Form $form): Form 47 | { 48 | return $form 49 | ->schema([ 50 | Select::make('user_id') 51 | ->relationship('user', config('filamentblog.user.columns.name')) 52 | ->required(), 53 | Textarea::make('comment') 54 | ->required() 55 | ->maxLength(65535) 56 | ->columnSpanFull(), 57 | Toggle::make('approved'), 58 | ]) 59 | ->columns(1); 60 | } 61 | 62 | public function table(Table $table): Table 63 | { 64 | return $table 65 | ->columns([ 66 | Tables\Columns\TextColumn::make('comment') 67 | ->searchable(), 68 | UserPhotoName::make('user') 69 | ->label('Commented By'), 70 | Tables\Columns\ToggleColumn::make('approved') 71 | ->beforeStateUpdated(function ($record, $state) { 72 | if ($state) { 73 | $record->approved_at = now(); 74 | } else { 75 | $record->approved_at = null; 76 | } 77 | 78 | return $state; 79 | }), 80 | Tables\Columns\TextColumn::make('approved_at') 81 | ->placeholder('Not approved') 82 | ->sortable(), 83 | 84 | Tables\Columns\TextColumn::make('created_at') 85 | ->dateTime() 86 | ->sortable() 87 | ->toggleable(isToggledHiddenByDefault: true), 88 | Tables\Columns\TextColumn::make('updated_at') 89 | ->dateTime() 90 | ->sortable() 91 | ->toggleable(isToggledHiddenByDefault: true), 92 | ]) 93 | ->filters([ 94 | Tables\Filters\SelectFilter::make('user') 95 | ->relationship('user', config('filamentblog.user.columns.name')) 96 | ->searchable() 97 | ->preload() 98 | ->multiple(), 99 | ]) 100 | ->headerActions([ 101 | Tables\Actions\CreateAction::make(), 102 | ]) 103 | ->actions([ 104 | Tables\Actions\ActionGroup::make([ 105 | Tables\Actions\EditAction::make(), 106 | Tables\Actions\ViewAction::make(), 107 | Tables\Actions\DeleteAction::make(), 108 | ]), 109 | ]) 110 | ->bulkActions([ 111 | Tables\Actions\BulkActionGroup::make([ 112 | Tables\Actions\DeleteBulkAction::make(), 113 | ]), 114 | ]); 115 | } 116 | 117 | public function infolist(Infolist $infolist): Infolist 118 | { 119 | return $infolist->schema([ 120 | Section::make('Comment') 121 | ->schema([ 122 | TextEntry::make('user.name') 123 | ->label('Commented by'), 124 | TextEntry::make('comment'), 125 | TextEntry::make('created_at'), 126 | TextEntry::make('approved_at')->label('Approved At')->placeholder('Not Approved'), 127 | 128 | ]) 129 | ->icon('heroicon-o-chat-bubble-left-ellipsis'), 130 | ]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Resources/PostResource/Pages/ViewPost.php: -------------------------------------------------------------------------------- 1 | getRecord(); 19 | 20 | return $record->title; 21 | } 22 | 23 | protected function getHeaderActions(): array 24 | { 25 | return [ 26 | Action::make('sendNotification') 27 | ->label('Send Notification') 28 | ->requiresConfirmation() 29 | ->icon('heroicon-o-bell')->action(function (Post $record) { 30 | event(new BlogPublished($record)); 31 | }) 32 | ->disabled(function (Post $record) { 33 | return $record->isNotPublished(); 34 | }), 35 | Action::make('preview') 36 | ->label('Preview') 37 | ->requiresConfirmation() 38 | ->icon('heroicon-o-eye')->url(function (Post $record) { 39 | return route('filamentblog.post.show', $record->slug); 40 | }, true) 41 | ->disabled(function (Post $record) { 42 | return $record->isNotPublished(); 43 | }), 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Resources/PostResource/RelationManagers/CommentsRelationManager.php: -------------------------------------------------------------------------------- 1 | schema([ 19 | Forms\Components\TextInput::make('comment') 20 | ->required() 21 | ->maxLength(255), 22 | ]); 23 | } 24 | 25 | public function table(Table $table): Table 26 | { 27 | return $table 28 | ->recordTitleAttribute('comment') 29 | ->columns([ 30 | Tables\Columns\TextColumn::make('comment')->limit(20), 31 | Tables\Columns\TextColumn::make('user.name'), 32 | ]) 33 | ->filters([ 34 | // 35 | ]) 36 | ->headerActions([ 37 | Tables\Actions\CreateAction::make(), 38 | ]) 39 | ->actions([ 40 | Tables\Actions\EditAction::make(), 41 | Tables\Actions\DeleteAction::make(), 42 | ]) 43 | ->bulkActions([ 44 | Tables\Actions\BulkActionGroup::make([ 45 | Tables\Actions\DeleteBulkAction::make(), 46 | ]), 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Resources/PostResource/RelationManagers/SeoDetailRelationManager.php: -------------------------------------------------------------------------------- 1 | schema(SeoDetail::getForm()); 19 | } 20 | 21 | public function table(Table $table): Table 22 | { 23 | return $table 24 | ->recordTitleAttribute('title') 25 | ->columns([ 26 | Tables\Columns\TextColumn::make('title'), 27 | Tables\Columns\TextColumn::make('description'), 28 | Tables\Columns\TextColumn::make('keywords')->badge(), 29 | ]) 30 | ->filters([ 31 | // 32 | ]) 33 | ->headerActions([ 34 | Tables\Actions\CreateAction::make(), 35 | ]) 36 | ->actions([ 37 | Tables\Actions\EditAction::make()->slideOver(), 38 | Tables\Actions\DeleteAction::make(), 39 | ]) 40 | ->bulkActions([ 41 | Tables\Actions\BulkActionGroup::make([ 42 | Tables\Actions\DeleteBulkAction::make(), 43 | ]), 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Resources/PostResource/Widgets/BlogPostPublishedChart.php: -------------------------------------------------------------------------------- 1 | count()), 14 | BaseWidget\Stat::make('Scheduled Post', Post::scheduled()->count()), 15 | BaseWidget\Stat::make('Pending Post', Post::pending()->count()), 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Resources/SeoDetailResource.php: -------------------------------------------------------------------------------- 1 | schema(SeoDetail::getForm()); 25 | } 26 | 27 | public static function table(Table $table): Table 28 | { 29 | return $table 30 | ->striped() 31 | ->columns([ 32 | Tables\Columns\TextColumn::make('post.title') 33 | ->limit(20), 34 | Tables\Columns\TextColumn::make('title') 35 | ->limit(20) 36 | ->searchable(), 37 | Tables\Columns\TextColumn::make('keywords')->badge() 38 | ->searchable(), 39 | Tables\Columns\TextColumn::make('created_at') 40 | ->dateTime() 41 | ->sortable() 42 | ->toggleable(isToggledHiddenByDefault: true), 43 | Tables\Columns\TextColumn::make('updated_at') 44 | ->dateTime() 45 | ->sortable() 46 | ->toggleable(isToggledHiddenByDefault: true), 47 | ]) 48 | ->defaultSort('id', 'desc') 49 | ->filters([ 50 | // 51 | ]) 52 | ->actions([ 53 | Tables\Actions\EditAction::make(), 54 | ]) 55 | ->bulkActions([ 56 | Tables\Actions\BulkActionGroup::make([ 57 | Tables\Actions\DeleteBulkAction::make(), 58 | ]), 59 | ]); 60 | } 61 | 62 | public static function getRelations(): array 63 | { 64 | return [ 65 | // 66 | ]; 67 | } 68 | 69 | public static function getPages(): array 70 | { 71 | return [ 72 | 'index' => \Firefly\FilamentBlog\Resources\SeoDetailResource\Pages\ListSeoDetails::route('/'), 73 | 'create' => \Firefly\FilamentBlog\Resources\SeoDetailResource\Pages\CreateSeoDetail::route('/create'), 74 | 'edit' => \Firefly\FilamentBlog\Resources\SeoDetailResource\Pages\EditSeoDetail::route('/{record}/edit'), 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Resources/SeoDetailResource/Pages/CreateSeoDetail.php: -------------------------------------------------------------------------------- 1 | schema(Setting::getForm()); 25 | } 26 | 27 | public static function canCreate(): bool 28 | { 29 | return Setting::count() === 0; 30 | } 31 | 32 | public static function table(Table $table): Table 33 | { 34 | return $table 35 | ->columns([ 36 | Tables\Columns\TextColumn::make('title') 37 | ->limit(25) 38 | ->searchable(), 39 | Tables\Columns\TextColumn::make('description') 40 | ->limit(30) 41 | ->searchable(), 42 | 43 | Tables\Columns\ImageColumn::make('logo'), 44 | 45 | Tables\Columns\TextColumn::make('organization_name'), 46 | 47 | Tables\Columns\TextColumn::make('created_at') 48 | ->dateTime() 49 | ->sortable() 50 | ->toggleable(isToggledHiddenByDefault: true), 51 | Tables\Columns\TextColumn::make('updated_at') 52 | ->dateTime() 53 | ->sortable() 54 | ->toggleable(isToggledHiddenByDefault: true), 55 | ]) 56 | ->filters([ 57 | // 58 | ]) 59 | ->actions([ 60 | Tables\Actions\EditAction::make(), 61 | Tables\Actions\ViewAction::make(), 62 | ]) 63 | ->bulkActions([ 64 | Tables\Actions\BulkActionGroup::make([ 65 | Tables\Actions\DeleteBulkAction::make(), 66 | ]), 67 | ]); 68 | } 69 | 70 | public static function getRelations(): array 71 | { 72 | return [ 73 | // 74 | ]; 75 | } 76 | 77 | public static function getPages(): array 78 | { 79 | return [ 80 | 'index' => \Firefly\FilamentBlog\Resources\SettingResource\Pages\ListSettings::route('/'), 81 | 'create' => \Firefly\FilamentBlog\Resources\SettingResource\Pages\CreateSetting::route('/create'), 82 | 'edit' => \Firefly\FilamentBlog\Resources\SettingResource\Pages\EditSetting::route('/{record}/edit'), 83 | ]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Resources/SettingResource/Pages/CreateSetting.php: -------------------------------------------------------------------------------- 1 | data); 15 | // } 16 | } 17 | -------------------------------------------------------------------------------- /src/Resources/SettingResource/Pages/EditSetting.php: -------------------------------------------------------------------------------- 1 | count() > 0); 25 | 26 | } 27 | 28 | public static function canDelete($record): bool 29 | { 30 | return false; 31 | } 32 | 33 | public static function form(Form $form): Form 34 | { 35 | return $form 36 | ->schema(ShareSnippet::getform()); 37 | } 38 | 39 | public static function table(Table $table): Table 40 | { 41 | return $table 42 | ->columns([ 43 | TextColumn::make('script_code') 44 | ->limit(50) 45 | ->searchable(), 46 | TextColumn::make('html_code') 47 | ->limit(50) 48 | ->searchable(), 49 | Tables\Columns\ToggleColumn::make('active'), 50 | ]) 51 | ->filters([ 52 | // 53 | ]) 54 | ->actions([ 55 | Tables\Actions\EditAction::make(), 56 | ]) 57 | ->bulkActions([ 58 | Tables\Actions\BulkActionGroup::make([ 59 | Tables\Actions\DeleteBulkAction::make(), 60 | ]), 61 | ]); 62 | } 63 | 64 | public static function getRelations(): array 65 | { 66 | return [ 67 | // 68 | ]; 69 | } 70 | 71 | public static function getPages(): array 72 | { 73 | return [ 74 | 'index' => \Firefly\FilamentBlog\Resources\ShareSnippetResource\Pages\ListShareSnippets::route('/'), 75 | 'edit' => \Firefly\FilamentBlog\Resources\ShareSnippetResource\Pages\EditShareSnippet::route('/{record}/edit'), 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Resources/ShareSnippetResource/Pages/CreateShareSnippet.php: -------------------------------------------------------------------------------- 1 | schema(Tag::getForm()); 25 | } 26 | 27 | public static function table(Table $table): Table 28 | { 29 | return $table 30 | ->columns([ 31 | Tables\Columns\TextColumn::make('name') 32 | ->searchable(), 33 | Tables\Columns\TextColumn::make('slug'), 34 | 35 | Tables\Columns\TextColumn::make('created_at') 36 | ->dateTime() 37 | ->sortable() 38 | ->toggleable(isToggledHiddenByDefault: true), 39 | 40 | Tables\Columns\TextColumn::make('updated_at') 41 | ->dateTime() 42 | ->sortable() 43 | ->toggleable(isToggledHiddenByDefault: true), 44 | ]) 45 | ->filters([ 46 | // 47 | ]) 48 | ->actions([ 49 | Tables\Actions\EditAction::make(), 50 | Tables\Actions\DeleteAction::make(), 51 | Tables\Actions\ViewAction::make(), 52 | ]) 53 | ->bulkActions([ 54 | Tables\Actions\BulkActionGroup::make([ 55 | Tables\Actions\DeleteBulkAction::make(), 56 | ]), 57 | ]); 58 | } 59 | 60 | public static function getRelations(): array 61 | { 62 | return [ 63 | // 64 | ]; 65 | } 66 | 67 | public static function getPages(): array 68 | { 69 | return [ 70 | 'index' => \Firefly\FilamentBlog\Resources\TagResource\Pages\ListTags::route('/'), 71 | 'edit' => \Firefly\FilamentBlog\Resources\TagResource\Pages\EditTag::route('/{record}/edit'), 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Resources/TagResource/Pages/CreateTag.php: -------------------------------------------------------------------------------- 1 | getTitle(); 24 | $description = $this->getDescription(); 25 | $keywords = $this->keywords; 26 | if ($title) { 27 | $html[] = "{$title}"; 28 | } 29 | if ($description) { 30 | $html[] = ""; 31 | } 32 | if (! empty($keywords)) { 33 | if ($keywords instanceof Collection) { 34 | $keywords = $keywords->toArray(); 35 | } 36 | 37 | $keywords = implode(', ', $keywords); 38 | $html[] = ""; 39 | } 40 | 41 | return implode(PHP_EOL, $html); 42 | } 43 | 44 | public function setTitle($title) 45 | { 46 | // open redirect vulnerability fix 47 | $title = str_replace(['http-equiv=', 'url='], '', $title); 48 | $title = strip_tags($title); 49 | $this->title = $title; 50 | 51 | return $this; 52 | } 53 | 54 | public function setDescription($description) 55 | { 56 | $this->description = ! $description ? $description : htmlspecialchars($description, ENT_QUOTES, 'UTF-8', false); 57 | 58 | return $this; 59 | } 60 | 61 | public function setKeywords(array $keywords) 62 | { 63 | // clean keywords 64 | $keywords = array_map('strip_tags', $keywords); 65 | // store keywords 66 | $this->keywords = $keywords; 67 | 68 | return $this; 69 | } 70 | 71 | public function getTitle() 72 | { 73 | return $this->title ?? $this->getDefaultTitle(); 74 | } 75 | 76 | private function getDescription() 77 | { 78 | return $this->description ?? $this->getDefaultDescription(); 79 | } 80 | 81 | public function getKeywords() 82 | { 83 | return $this->keywords ?? $this->getDefaultKeywords(); 84 | } 85 | 86 | public function getDefaultTitle() 87 | { 88 | return $this->config->get('filamentblog.seo.meta.title'); 89 | } 90 | 91 | private function getDefaultDescription() 92 | { 93 | return $this->config->get('filamentblog.seo.meta.description'); 94 | } 95 | 96 | public function getDefaultKeywords() 97 | { 98 | return $this->config->get('filamentblog.seo.meta.keywords'); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Services/SEOService.php: -------------------------------------------------------------------------------- 1 | title = $title; 10 | } 11 | 12 | public function setDescription(string $description) 13 | { 14 | $this->description = $description; 15 | } 16 | 17 | public function getTitle() 18 | { 19 | return $this->title; 20 | } 21 | 22 | public function getDescription() 23 | { 24 | return $this->description; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Tables/Columns/UserPhotoName.php: -------------------------------------------------------------------------------- 1 | {config('filamentblog.user.columns.name')}; 13 | } 14 | 15 | public function getAvatarAttribute() 16 | { 17 | return $this->{config('filamentblog.user.columns.avatar')} 18 | ? asset('storage/'.$this->{config('filamentblog.user.columns.avatar')}) : 'https://ui-avatars.com/api/?&background=random&name='.$this->{config('filamentblog.user.columns.name')}; 19 | } 20 | 21 | public function posts() 22 | { 23 | return $this->hasMany(Post::class, config('filamentblog.user.foreign_key')); 24 | } 25 | 26 | public function comments() 27 | { 28 | return $this->hasMany(Comment::class, config('filamentblog.user.foreign_key')); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/Feature/Models/CategoryTest.php: -------------------------------------------------------------------------------- 1 | hasAttached(Post::factory()->count(3)) 12 | ->create(); 13 | 14 | // Act & Assert 15 | expect($category->posts) 16 | ->toHaveCount(3) 17 | ->each 18 | ->toBeInstanceOf(Post::class); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Feature/Models/CommentTest.php: -------------------------------------------------------------------------------- 1 | published()->create(); 11 | Post::factory()->create(); 12 | 13 | // Act & Assert 14 | 15 | expect(Post::published()->count())->toBe(1); 16 | 17 | }); 18 | 19 | it('has categories', function () { 20 | // Arrange 21 | $post = Post::factory() 22 | ->hasAttached(Category::factory()->count(3)) 23 | ->create(); 24 | 25 | // Act & Assert 26 | expect($post->categories) 27 | ->toHaveCount(3) 28 | ->each 29 | ->toBeInstanceOf(Category::class); 30 | }); 31 | 32 | it('has tags', function () { 33 | // Arrange 34 | $post = Post::factory() 35 | ->hasAttached(Tag::factory()->count(3)) 36 | ->create(); 37 | 38 | // Act & Assert 39 | expect($post->tags) 40 | ->toHaveCount(3) 41 | ->each 42 | ->toBeInstanceOf(Tag::class); 43 | }); 44 | 45 | it('has seoDetail', function () { 46 | // Arrange 47 | $post = Post::factory()->has(SeoDetail::factory(1)) 48 | ->create(); 49 | 50 | // Act & Assert 51 | expect($post->seoDetail) 52 | ->toBeInstanceOf(SeoDetail::class); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /tests/Feature/Models/SeoDeatilTest.php: -------------------------------------------------------------------------------- 1 | has(SeoDetail::factory())->create(); 9 | 10 | // Act & Assert 11 | expect($post->seoDetail)->toBeInstanceOf(SeoDetail::class); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/Feature/Models/TagTest.php: -------------------------------------------------------------------------------- 1 | hasAttached(Post::factory()->count(3)) 10 | ->create(); 11 | 12 | // Act & Assert 13 | expect($tag->posts) 14 | ->toHaveCount(3) 15 | ->each 16 | ->toBeInstanceOf(Post::class); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/Feature/NewPostPublishedMailTest.php: -------------------------------------------------------------------------------- 1 | post = Post::factory()->published()->create(); 13 | }); 14 | it('check event listener is attached to the event', function () { 15 | // Arrange 16 | $post = Post::factory()->published()->create(); 17 | 18 | // Assert 19 | Event::fake(); 20 | event(new \Firefly\FilamentBlog\Events\BlogPublished($post)); 21 | 22 | Event::assertDispatched(\Firefly\FilamentBlog\Events\BlogPublished::class); 23 | 24 | Event::assertListening( 25 | \Firefly\FilamentBlog\Events\BlogPublished::class, 26 | SendBlogPublishedNotification::class 27 | ); 28 | 29 | }); 30 | it('send new post published email to news letter subscriber', function () { 31 | 32 | //Arrange 33 | $post = Post::factory()->published()->create(); 34 | NewsLetter::factory()->count(3)->create(); 35 | $subscribers = NewsLetter::all(); 36 | 37 | Mail::fake(); 38 | 39 | //Assert 40 | foreach ($subscribers as $subscriber) { 41 | Mail::send(new BlogPublished($post, $subscriber->email)); 42 | Mail::assertSent(BlogPublished::class); 43 | 44 | } 45 | }); 46 | 47 | it('includes post details on email template', function () { 48 | 49 | // Arrange 50 | $post = Post::factory()->published()->create(); 51 | $subscriber = NewsLetter::factory()->create(); 52 | $mail = new BlogPublished($post, $subscriber->email); 53 | 54 | // Assert 55 | $mail->assertSeeInHtml('Thank you for subscribing to our blog updates!'); 56 | $mail->assertSeeInHtml($post->title); 57 | $mail->assertSeeInHtml($post->featurePhoto); 58 | $mail->assertSeeInHtml('Read More'); 59 | $mail->assertSeeInHtml(route('filamentblog.post.show', $post->slug)); 60 | 61 | }); 62 | it('throws exception if post is not published', function () { 63 | // Arrange 64 | $post = Post::factory()->create(); 65 | $subscriber = NewsLetter::factory()->create(); 66 | $mail = new BlogPublished($post, $subscriber->email); 67 | 68 | // Assert 69 | expect(fn () => $mail->envelope())->toThrow(CannotSendEmail::class); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/Feature/Pages/AllPageTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | // dd($setting); 8 | }); 9 | it('return success for all post page', function () { 10 | \Pest\Laravel\withoutExceptionHandling(); 11 | get(route('filamentblog.post.all')) 12 | ->assertOk(); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/Feature/Pages/CategoryPageTest.php: -------------------------------------------------------------------------------- 1 | hasAttached(Post::factory()->published()->count(3)->state(new Sequence( 16 | ['title' => 'First Post', 'slug' => 'first-post'], 17 | ['title' => 'Second Post', 'slug' => 'second-post'], 18 | ['title' => 'Third Post', 'slug' => 'third-post'], 19 | ))) 20 | ->create(); 21 | 22 | get(route('filamentblog.category.post', $category)) 23 | ->assertSeeText(['First Post', 'Second Post', 'Third Post']) 24 | ->assertOk(); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Feature/Pages/HomePageTest.php: -------------------------------------------------------------------------------- 1 | hasAttached(Category::factory()->count(1)) 16 | ->published() 17 | ->create(); 18 | 19 | $secondPost = Post::factory()->published()->create(); 20 | $thirdPost = Post::factory()->pending()->create([ 21 | 'title' => 'Pending Post', 22 | 'sub_title' => 'This is a pending post', 23 | ]); 24 | 25 | // Act & Assert 26 | get(route('filamentblog.post.index')) 27 | ->assertSeeText([ 28 | $firstPost->title, 29 | $firstPost->sub_title, 30 | $firstPost->formattedPublishedDate(), 31 | $firstPost->user->name, 32 | $firstPost->categories->first()->name, 33 | 34 | $secondPost->title, 35 | $secondPost->sub_title, 36 | $secondPost->formattedPublishedDate(), 37 | $secondPost->user->name, 38 | ]) 39 | ->assertDontSeeText([ 40 | $thirdPost->title, 41 | $thirdPost->sub_title, 42 | ]); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/Feature/Pages/PostDetailsPageTest.php: -------------------------------------------------------------------------------- 1 | pending()->create(); 13 | 14 | // Act & Assert 15 | get(route('filamentblog.post.show', $post)) 16 | ->assertNotFound(); 17 | }); 18 | 19 | it('does not found scheduled post', function () { 20 | // Arrange 21 | $post = Post::factory()->scheduled()->create(); 22 | 23 | // Act & Assert 24 | get(route('filamentblog.post.show', $post)) 25 | ->assertNotFound(); 26 | }); 27 | 28 | it('show published post details', function () { 29 | $this->withoutExceptionHandling(); 30 | 31 | // Arrange 32 | $post = Post::factory() 33 | ->published() 34 | ->hasAttached(Category::factory()->count(1)) 35 | ->hasSeoDetail([ 36 | 'description' => 'This is a description for the post', 37 | ]) 38 | ->hasComments([ 39 | 'approved' => true, 40 | ]) 41 | ->create(); // Act & Assert 42 | 43 | get(route('filamentblog.post.show', $post)) 44 | ->assertSeeText([ 45 | $post->title, 46 | $post->sub_title, 47 | $post->formattedPublishedDate(), 48 | $post->user->name, 49 | $post->categories->first()->name, 50 | $post->comments->first()->comment, 51 | 'Related Posts', 52 | 'Leave a reply', 53 | ])->assertSee([ 54 | $post->seoDetail->description, 55 | ]); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/Feature/Pages/SearchPageTest.php: -------------------------------------------------------------------------------- 1 | published()->count(2)->state(new Sequence([ 13 | 'title' => 'First Post', 14 | 'slug' => 'first-post', 15 | ], [ 16 | 'title' => 'Second Post', 17 | 'slug' => 'second-post', 18 | ]))->create(); 19 | get(route('filamentblog.post.search', ['query' => 'First Post'])) 20 | ->assertSeeText('First Post') 21 | ->assertDontSee('Second Post'); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /tests/Feature/Pages/TagPageTest.php: -------------------------------------------------------------------------------- 1 | hasAttached(Post::factory()->published()->count(3)->state(new Sequence( 16 | ['title' => 'First Post', 'slug' => 'first-post'], 17 | ['title' => 'Second Post', 'slug' => 'second-post'], 18 | ['title' => 'Third Post', 'slug' => 'third-post'], 19 | ))) 20 | ->create(); 21 | 22 | get(route('filamentblog.tag.post', $category)) 23 | ->assertSeeText(['First Post', 'Second Post', 'Third Post']) 24 | ->assertOk(); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Feature/UserCommentTest.php: -------------------------------------------------------------------------------- 1 | post = Post::factory()->published()->create(); 12 | $this->user = User::factory()->create(); 13 | $this->comment = [ 14 | 'comment' => 'This is a comment', 15 | 'user_id' => $this->user->id, 16 | ]; 17 | }); 18 | it('not allow for un authenticated user to comment on post', function () { 19 | $this->withoutExceptionHandling(); 20 | 21 | //Arrange 22 | 23 | $this->expectException(RouteNotFoundException::class); 24 | $this->expectExceptionMessage('Route [login] not defined.'); 25 | 26 | // Act & Assert 27 | post(route('filamentblog.comment.store', $this->post), $this->comment); 28 | }); 29 | 30 | it('only allow authenticated user to comment on post', function () { 31 | $this->withoutExceptionHandling(); 32 | 33 | // Act 34 | actingAs($this->user); 35 | 36 | expect(post(route('filamentblog.comment.store', $this->post), $this->comment)) 37 | ->assertRedirectToRoute('filamentblog.post.show', $this->post); 38 | 39 | // Assert 40 | $this->assertDatabaseHas('comments', $this->comment); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/Feature/UserNewsLetterSubscriptionTest.php: -------------------------------------------------------------------------------- 1 | 'johndeo@example.com', 8 | ]; 9 | post(route('filamentblog.post.subscribe'), $data) 10 | ->assertRedirect()->assertSessionHas('success', 'You have successfully subscribed to our news letter'); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | expect()->extend('toBeOne', function () { 31 | return $this->toBe(1); 32 | }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | function setSettingData() 46 | { 47 | \Firefly\FilamentBlog\Models\Setting::factory()->create(); 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | --------------------------------------------------------------------------------