├── database ├── .gitignore ├── seeders │ └── DatabaseSeeder.php ├── migrations │ ├── 2024_07_16_160942_create_pages_table.php │ ├── 2024_07_17_130705_create_settings_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 2024_07_16_160942_create_posts_table.php │ ├── 0001_01_01_000000_create_users_table.php │ ├── 2024_08_02_084106_ensure_default_settings_and_data.php │ └── 0001_01_01_000002_create_jobs_table.php └── factories │ └── UserFactory.php ├── bootstrap ├── cache │ └── .gitignore ├── providers.php └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore ├── debugbar │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── public ├── robots.txt ├── favicon.ico ├── index.php ├── js │ └── filament │ │ ├── forms │ │ └── components │ │ │ ├── textarea.js │ │ │ ├── tags-input.js │ │ │ └── key-value.js │ │ └── tables │ │ └── components │ │ └── table.js ├── .htaccess └── css │ └── filament │ └── support │ └── support.css ├── resources ├── js │ ├── bootstrap.js │ ├── lib │ │ └── utils.js │ ├── Components │ │ ├── blocks │ │ │ ├── NotFoundSection.vue │ │ │ ├── Image.vue │ │ │ ├── RichEditor.vue │ │ │ ├── LogoSection.vue │ │ │ ├── StatsSection.vue │ │ │ ├── FeaturelistSection.vue │ │ │ ├── FaqSection.vue │ │ │ ├── HeroSection.vue │ │ │ ├── TitleSection.vue │ │ │ ├── DetailSection.vue │ │ │ └── BlogSection.vue │ │ ├── ui │ │ │ └── dialog │ │ │ │ ├── DialogClose.vue │ │ │ │ ├── DialogHeader.vue │ │ │ │ ├── DialogTrigger.vue │ │ │ │ ├── DialogFooter.vue │ │ │ │ ├── Dialog.vue │ │ │ │ ├── index.js │ │ │ │ ├── DialogDescription.vue │ │ │ │ ├── DialogTitle.vue │ │ │ │ ├── DialogScrollContent.vue │ │ │ │ └── DialogContent.vue │ │ ├── EnhancedLink.vue │ │ ├── UButton.vue │ │ ├── SEO.vue │ │ ├── Renderer.vue │ │ └── Pagination.vue │ ├── Pages │ │ ├── Page.vue │ │ ├── Error.vue │ │ └── Blog │ │ │ └── Show.vue │ ├── Layouts │ │ └── BaseLayout.vue │ ├── app.js │ ├── ssr.js │ └── Helpers │ │ └── Asset.js ├── views │ ├── app.blade.php │ └── vendor │ │ └── livewire │ │ ├── simple-bootstrap.blade.php │ │ └── simple-tailwind.blade.php └── css │ └── app.css ├── data ├── blog.json ├── helloworld.json └── home.json ├── postcss.config.js ├── app ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ ├── BlogController.php │ │ └── PageController.php │ ├── Resources │ │ ├── PageResource.php │ │ ├── PostResource.php │ │ └── BaseTranslatableResource.php │ ├── Middleware │ │ └── HandleInertiaRequests.php │ └── CacheProfiles │ │ └── InertiaAwareCacheProfile.php ├── functions.php ├── Models │ ├── BaseModel.php │ ├── Page.php │ ├── Post.php │ ├── User.php │ └── Setting.php ├── Filament │ ├── BlockGroups │ │ ├── RichContent.php │ │ ├── BaseGroup.php │ │ └── Properties.php │ ├── Blocks │ │ ├── RichContentBlocks │ │ │ ├── BlogSection.php │ │ │ ├── ModulesSection.php │ │ │ ├── Image.php │ │ │ ├── FaqSection.php │ │ │ ├── StatsSection.php │ │ │ ├── FeaturelistSection.php │ │ │ ├── RichEditor.php │ │ │ ├── LogoSection.php │ │ │ ├── TestimonialSection.php │ │ │ ├── HeroSection.php │ │ │ ├── TitleSection.php │ │ │ ├── PricingSection.php │ │ │ └── DetailSection.php │ │ └── BaseBlock.php │ └── Resources │ │ ├── PageResource │ │ └── Pages │ │ │ ├── CreatePage.php │ │ │ ├── ListPages.php │ │ │ └── EditPage.php │ │ ├── PostResource │ │ └── Pages │ │ │ ├── CreatePost.php │ │ │ ├── ListPosts.php │ │ │ └── EditPost.php │ │ ├── SettingResource │ │ └── Pages │ │ │ ├── CreateSetting.php │ │ │ ├── ListSettings.php │ │ │ └── EditSetting.php │ │ ├── Traits │ │ └── Copyable.php │ │ ├── PageResource.php │ │ ├── PostResource.php │ │ └── SettingResource.php ├── Providers │ ├── AppServiceProvider.php │ └── Filament │ │ └── AdminPanelProvider.php └── Console │ └── Commands │ └── EnsureDefaultSettings.php ├── tests ├── TestCase.php ├── Unit │ └── ExampleTest.php └── Feature │ ├── ExampleTest.php │ ├── Auth │ ├── RegistrationTest.php │ ├── PasswordConfirmationTest.php │ ├── AuthenticationTest.php │ ├── PasswordUpdateTest.php │ ├── EmailVerificationTest.php │ └── PasswordResetTest.php │ └── ProfileTest.php ├── .gitattributes ├── routes ├── console.php ├── web.php └── auth.php ├── jsconfig.json ├── .editorconfig ├── .gitignore ├── artisan ├── components.json ├── vite.config.js ├── package.json ├── config ├── services.php ├── sitemap.php ├── inertia.php ├── filesystems.php ├── settings.php ├── responsecache.php ├── cache.php ├── mail.php ├── queue.php ├── auth.php ├── app.php └── logging.php ├── phpunit.xml ├── .env.example ├── composer.json └── tailwind.config.js /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/debugbar/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | window.axios = axios; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SabatinoMasala/filament-marketing-starter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /data/blog.json: -------------------------------------------------------------------------------- 1 | {"en":[{"type":"blog_section","data":{"per_page":9}}], "fr":[{"type":"blog_section","data":{"per_page":9}}]} 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
{{ data }}
4 |
5 | 6 | 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote')->hourly(); 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"], 6 | "ziggy-js": ["./vendor/tightenco/ziggy"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "public"] 10 | } 11 | -------------------------------------------------------------------------------- /app/Models/BaseModel.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /bootstrap/ssr 3 | /node_modules 4 | /public/build 5 | /public/hot 6 | /public/storage 7 | /storage/*.key 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpactor.json 13 | .phpunit.result.cache 14 | Homestead.json 15 | Homestead.yaml 16 | auth.json 17 | npm-debug.log 18 | yarn-error.log 19 | /.fleet 20 | /.idea 21 | /.vscode 22 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/Filament/BlockGroups/RichContent.php: -------------------------------------------------------------------------------- 1 | dirname(__DIR__) . '/Blocks/RichContentBlocks', 11 | 'fqn' => '\\App\\Filament\\Blocks\\RichContentBlocks\\', 12 | ]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /app/Http/Resources/PageResource.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function toArray(Request $request): array 15 | { 16 | return parent::toArray($request); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/BlogSection.php: -------------------------------------------------------------------------------- 1 | numeric()->default(9), 14 | ]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/ModulesSection.php: -------------------------------------------------------------------------------- 1 | numeric()->default(9), 14 | ]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "default", 4 | "typescript": false, 5 | "tsConfigPath": "./jsconfig.json", 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "resources/css/app.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "framework": "laravel", 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /resources/js/Pages/Page.vue: -------------------------------------------------------------------------------- 1 | 7 | 17 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/Components/EnhancedLink.vue: -------------------------------------------------------------------------------- 1 | 6 | 15 | -------------------------------------------------------------------------------- /app/Http/Resources/PostResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return parent::toArray($request); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/Layouts/BaseLayout.vue: -------------------------------------------------------------------------------- 1 | 8 | 16 | 21 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/textarea.js: -------------------------------------------------------------------------------- 1 | function i({initialHeight:t}){return{height:t+"rem",init:function(){this.setInitialHeight(),this.setUpResizeObserver()},setInitialHeight:function(){this.height=t+"rem",!(this.$el.scrollHeight<=0)&&(this.$el.style.height=this.height)},resize:function(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.height!==e&&(this.height=e,this.$el.style.height=this.height)},setUpResizeObserver:function(){new ResizeObserver(()=>{this.height=this.$el.style.height}).observe(this.$el)}}}export{i as default}; 2 | -------------------------------------------------------------------------------- /app/Http/Controllers/BlogController.php: -------------------------------------------------------------------------------- 1 | ' . LaravelLocalization::getCurrentLocale(), $slug)->firstOrFail(); 15 | return Inertia::render('Blog/Show', [ 16 | 'post' => PostResource::make($post), 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/Image.php: -------------------------------------------------------------------------------- 1 | image() 16 | ->required(), 17 | TextInput::make('alt') 18 | ->required(), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Filament/Resources/PageResource/Pages/CreatePage.php: -------------------------------------------------------------------------------- 1 | getShortName()); 13 | } 14 | 15 | static function make(Form $form) 16 | { 17 | return Builder\Block::make(static::getName()) 18 | ->schema(static::schema($form)); 19 | } 20 | 21 | static function schema(Form $form) 22 | { 23 | return []; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/Filament/Resources/SettingResource/Pages/CreateSetting.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | User::factory()->create([ 19 | 'name' => 'Test User', 20 | 'email' => 'masalasabatino@gmail.com', 21 | 'password' => bcrypt('masalasabatino@gmail.com'), 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/js/Components/UButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 18 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ c('seo.title') }} 8 | 9 | 10 | 11 | 12 | @routes 13 | @vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]) 14 | @inertiaHead 15 | 16 | 17 | @inertia 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/FaqSection.php: -------------------------------------------------------------------------------- 1 | schema([ 16 | Forms\Components\TextInput::make('question'), 17 | Forms\Components\RichEditor::make('answer'), 18 | ]), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | schema([ 16 | Forms\Components\TextInput::make('label'), 17 | Forms\Components\TextInput::make('quantity'), 18 | ])->cloneable(), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | redirectTo('/en'); 11 | }); 12 | 13 | Route::group([ 14 | 'prefix' => LaravelLocalization::setLocale(), 15 | 'middleware' => [ 16 | CacheResponse::class 17 | ] 18 | ], function() { 19 | Route::group([ 20 | 'prefix' => c('routes.blog'), 21 | ], function() { 22 | Route::get('/{slug}', [BlogController::class, 'show']); 23 | }); 24 | Route::get('/{any?}', [PageController::class, 'show'])->where('any', '.*'); 25 | }); 26 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 31 | -------------------------------------------------------------------------------- /app/Models/Page.php: -------------------------------------------------------------------------------- 1 | getTranslation('slug', $locale); 16 | if ($slug === 'home') { 17 | $slug = ''; 18 | } 19 | $url = '/' . $locale; 20 | if (!empty($slug)) { 21 | $url .= '/' . $slug; 22 | } 23 | ResponseCache::selectCachedItems()->usingSuffix('inertia')->forUrls([$url])->forget(); 24 | ResponseCache::selectCachedItems()->usingSuffix('no-inertia')->forUrls([$url])->forget(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/tags-input.js: -------------------------------------------------------------------------------- 1 | function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default}; 2 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/FeaturelistSection.php: -------------------------------------------------------------------------------- 1 | schema([ 17 | Forms\Components\TextInput::make('title'), 18 | Forms\Components\RichEditor::make('description'), 19 | ]) 20 | ->collapsed() 21 | ->collapsible() 22 | ->cloneable(), 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Models/Post.php: -------------------------------------------------------------------------------- 1 | getTranslation('slug', $locale); 20 | $url = '/' . $locale . c('routes.blog'); 21 | if (!empty($slug)) { 22 | $url .= '/' . $slug; 23 | } 24 | ResponseCache::selectCachedItems()->usingSuffix('inertia')->forUrls([$url])->forget(); 25 | ResponseCache::selectCachedItems()->usingSuffix('no-inertia')->forUrls([$url])->forget(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/Filament/Resources/PageResource/Pages/EditPage.php: -------------------------------------------------------------------------------- 1 | getCopyAction(['title', 'slug', 'content', 'seo']), 21 | Actions\LocaleSwitcher::make(), 22 | Actions\DeleteAction::make(), 23 | ]; 24 | } 25 | 26 | public function afterSave() 27 | { 28 | $this->record->clearCache($this->activeLocale); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2024_07_16_160942_create_pages_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->json('title'); 17 | $table->json('slug'); 18 | $table->json('content'); 19 | $table->json('seo')->nullable(); 20 | $table->json('meta')->nullable(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('pages'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 15 | 16 | $response->assertStatus(200); 17 | } 18 | 19 | public function test_new_users_can_register(): void 20 | { 21 | $response = $this->post('/register', [ 22 | 'name' => 'Test User', 23 | 'email' => 'test@example.com', 24 | 'password' => 'password', 25 | 'password_confirmation' => 'password', 26 | ]); 27 | 28 | $this->assertAuthenticated(); 29 | $response->assertRedirect(route('dashboard', absolute: false)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/RichEditor.php: -------------------------------------------------------------------------------- 1 | toolbarButtons([ 14 | 'attachFiles', 15 | 'blockquote', 16 | 'bold', 17 | 'bulletList', 18 | 'codeBlock', 19 | 'h1', 20 | 'h2', 21 | 'h3', 22 | 'italic', 23 | 'link', 24 | 'orderedList', 25 | 'redo', 26 | 'strike', 27 | 'underline', 28 | 'undo', 29 | ]), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Filament/Resources/SettingResource/Pages/EditSetting.php: -------------------------------------------------------------------------------- 1 | getCopyAction(['value']), 22 | Actions\LocaleSwitcher::make(), 23 | Actions\DeleteAction::make(), 24 | ]; 25 | } 26 | 27 | public function afterSave() 28 | { 29 | ResponseCache::clear(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/Filament/Resources/PostResource/Pages/EditPost.php: -------------------------------------------------------------------------------- 1 | getCopyAction(['title', 'slug', 'short_description', 'content', 'published_at', 'seo']), 21 | Actions\LocaleSwitcher::make(), 22 | Actions\DeleteAction::make(), 23 | ]; 24 | } 25 | 26 | public function afterSave() 27 | { 28 | $this->record->clearCache($this->activeLocale); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/LogoSection.php: -------------------------------------------------------------------------------- 1 | default('Our Partners'), 15 | Forms\Components\Select::make('background')->options([ 16 | 'default' => 'Default', 17 | 'white' => 'White', 18 | ])->default('default'), 19 | Forms\Components\Repeater::make('logos')->schema([ 20 | Forms\Components\TextInput::make('title'), 21 | FileUpload::make('url') 22 | ->image() 23 | ->required(), 24 | ]), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2024_07_17_130705_create_settings_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('key'); 19 | $table->string('group')->index(); 20 | $table->json('value')->nullable(); 21 | $table->unique(['key', 'group']); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('settings'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /app/Http/Resources/BaseTranslatableResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | $attributes = $this->attributesToArray(); // attributes selected by the query 18 | // remove attributes if they are not selected 19 | $translatables = array_filter($this->getTranslatableAttributes(), function ($key) use ($attributes) { 20 | return array_key_exists($key, $attributes); 21 | }); 22 | foreach ($translatables as $field) { 23 | $attributes[$field] = $this->getTranslation($field, \App::getLocale()); 24 | } 25 | return array_merge($attributes, $this->relationsToArray()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/js/Components/SEO.vue: -------------------------------------------------------------------------------- 1 | 17 | 25 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/key-value.js: -------------------------------------------------------------------------------- 1 | function r({state:o}){return{state:o,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows);this.rows=[];let s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.$nextTick(()=>{this.rows=e,this.updateState()})},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build && vite build --ssr" 7 | }, 8 | "devDependencies": { 9 | "@inertiajs/vue3": "^1.0.0", 10 | "@tailwindcss/forms": "^0.5.3", 11 | "@vitejs/plugin-vue": "^5.0.0", 12 | "@vue/server-renderer": "^3.4.0", 13 | "autoprefixer": "^10.4.12", 14 | "axios": "^1.6.4", 15 | "laravel-vite-plugin": "^1.0", 16 | "postcss": "^8.4.31", 17 | "tailwindcss": "^3.2.1", 18 | "vite": "^5.0", 19 | "vue": "^3.4.0" 20 | }, 21 | "dependencies": { 22 | "@headlessui/vue": "^1.7.22", 23 | "@heroicons/vue": "^2.1.5", 24 | "class-variance-authority": "^0.7.0", 25 | "clsx": "^2.1.1", 26 | "lucide-vue-next": "^0.408.0", 27 | "moment": "^2.30.1", 28 | "radix-vue": "^1.9.1", 29 | "tailwind-merge": "^2.4.0", 30 | "tailwindcss-animate": "^1.0.7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | import '../css/app.css'; 3 | 4 | import { createApp, h } from 'vue'; 5 | import { createInertiaApp } from '@inertiajs/vue3'; 6 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; 7 | import { ZiggyVue } from '../../vendor/tightenco/ziggy'; 8 | 9 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; 10 | console.log(import.meta.env.VITE_CDN_URL); 11 | import asset from '@/Helpers/Asset'; 12 | 13 | createInertiaApp({ 14 | title: (title) => `${title} - ${appName}`, 15 | resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')), 16 | setup({ el, App, props, plugin }) { 17 | const app = createApp({ render: () => h(App, props) }) 18 | .use(plugin) 19 | .use(ZiggyVue); 20 | app.config.globalProperties.asset = (path, recipe) => { 21 | return asset(path, recipe); 22 | } 23 | return app.mount(el); 24 | }, 25 | progress: { 26 | color: '#4B5563', 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/TestimonialSection.php: -------------------------------------------------------------------------------- 1 | schema([ 18 | Forms\Components\TextInput::make('rating')->numeric()->minValue(0)->maxValue(5)->step(1), 19 | Forms\Components\RichEditor::make('review'), 20 | FileUpload::make('picture')->image(), 21 | TextInput::make('name'), 22 | ]) 23 | ->collapsed() 24 | ->cloneable() 25 | ->collapsible(), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/js/Components/Renderer.vue: -------------------------------------------------------------------------------- 1 | 6 | 33 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/Image.vue: -------------------------------------------------------------------------------- 1 | 9 | 22 | -------------------------------------------------------------------------------- /database/migrations/2024_07_16_160942_create_posts_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->json('title'); 17 | $table->json('slug'); 18 | $table->json('content'); 19 | $table->json('short_description'); 20 | $table->timestamp('published_at')->nullable(); 21 | $table->string('featured_image')->nullable()->after('short_description'); 22 | $table->json('seo')->nullable(); 23 | $table->json('meta')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('posts'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/HeroSection.php: -------------------------------------------------------------------------------- 1 | options([ 15 | 'black' => 'Black', 16 | 'primary' => 'Primary', 17 | 'secondary' => 'Secondary', 18 | 'white' => 'White', 19 | ]), 20 | FileUpload::make('hero_image')->image(), 21 | Forms\Components\TextInput::make('hero_title'), 22 | Forms\Components\RichEditor::make('hero_content'), 23 | Forms\Components\Repeater::make('buttons')->schema([ 24 | Forms\Components\TextInput::make('label'), 25 | Forms\Components\TextInput::make('url'), 26 | Forms\Components\TextInput::make('variant'), 27 | ]), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/RichEditor.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | 49 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/LogoSection.vue: -------------------------------------------------------------------------------- 1 | 13 | 18 | 26 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /app/Filament/Resources/Traits/Copyable.php: -------------------------------------------------------------------------------- 1 | form([ 13 | Select::make('locale')->options([ 14 | 'nl' => 'Dutch', 15 | 'en' => 'English', 16 | 'fr' => 'French', 17 | ]) 18 | ->default('nl') 19 | ->required(), 20 | ]) 21 | ->action(function (array $data) use ($fieldsToCopy) { 22 | $propsToCopy = $fieldsToCopy; 23 | collect($propsToCopy)->each(function($prop) use ($data) { 24 | $dataToCopy = $this->record->getTranslations()[$prop][$data['locale']]; 25 | $this->record->setTranslation($prop, $this->activeLocale, $dataToCopy); 26 | }); 27 | $this->record->save(); 28 | }) 29 | ->after(fn ($livewire) => $this->fillForm()) 30 | ->link(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/TitleSection.php: -------------------------------------------------------------------------------- 1 | options([ 16 | 'default' => 'Default', 17 | 'plain' => 'Plain', 18 | ])->default('default'), 19 | Forms\Components\RichEditor::make('description'), 20 | Forms\Components\Repeater::make('buttons')->schema([ 21 | Forms\Components\TextInput::make('label'), 22 | Forms\Components\TextInput::make('url'), 23 | Forms\Components\Select::make('variant')->options([ 24 | 'red' => 'Red', 25 | 'transparent' => 'Transparent', 26 | ]), 27 | ]) 28 | ->collapsed() 29 | ->collapsible() 30 | ->cloneable(), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's email address should be unverified. 37 | */ 38 | public function unverified(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'email_verified_at' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /resources/js/ssr.js: -------------------------------------------------------------------------------- 1 | import { createSSRApp, h } from 'vue'; 2 | import { renderToString } from '@vue/server-renderer'; 3 | import { createInertiaApp } from '@inertiajs/vue3'; 4 | import createServer from '@inertiajs/vue3/server'; 5 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; 6 | import { ZiggyVue } from '../../vendor/tightenco/ziggy'; 7 | 8 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; 9 | import asset from '@/Helpers/Asset'; 10 | 11 | createServer((page) => 12 | createInertiaApp({ 13 | page, 14 | render: renderToString, 15 | title: (title) => `${title} - ${appName}`, 16 | resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')), 17 | setup({ App, props, plugin }) { 18 | const app = createSSRApp({ render: () => h(App, props) }) 19 | .use(plugin) 20 | .use(ZiggyVue, { 21 | ...page.props.ziggy, 22 | location: new URL(page.props.ziggy.location), 23 | }); 24 | app.config.globalProperties.asset = (path, recipe) => { 25 | return asset(path, recipe); 26 | } 27 | return app; 28 | }, 29 | }), 30 | 13715 31 | ); 32 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $response = $this->actingAs($user)->get('/confirm-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_password_can_be_confirmed(): void 23 | { 24 | $user = User::factory()->create(); 25 | 26 | $response = $this->actingAs($user)->post('/confirm-password', [ 27 | 'password' => 'password', 28 | ]); 29 | 30 | $response->assertRedirect(); 31 | $response->assertSessionHasNoErrors(); 32 | } 33 | 34 | public function test_password_is_not_confirmed_with_invalid_password(): void 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this->actingAs($user)->post('/confirm-password', [ 39 | 'password' => 'wrong-password', 40 | ]); 41 | 42 | $response->assertSessionHasErrors(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Filament/BlockGroups/BaseGroup.php: -------------------------------------------------------------------------------- 1 | blocks($blocks) 34 | ->collapsible() 35 | ->cloneable(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/PricingSection.php: -------------------------------------------------------------------------------- 1 | schema([ 16 | Forms\Components\TextInput::make('title'), 17 | Forms\Components\Checkbox::make('most_popular'), 18 | Forms\Components\TextInput::make('button_title'), 19 | Forms\Components\TextInput::make('button_link'), 20 | Forms\Components\TextInput::make('price_monthly')->numeric(), 21 | Forms\Components\TextInput::make('price_yearly')->numeric(), 22 | ])->cloneable(), 23 | Forms\Components\Repeater::make('features')->schema([ 24 | Forms\Components\TextInput::make('title'), 25 | Forms\Components\RichEditor::make('more_info'), 26 | Forms\Components\Textarea::make('tiers')->helperText('Separate each tier with a comma.'), 27 | ])->cloneable() 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Filament/Blocks/RichContentBlocks/DetailSection.php: -------------------------------------------------------------------------------- 1 | image(), 17 | Forms\Components\Select::make('image_position')->options([ 18 | 'left' => 'Left', 19 | 'right' => 'Right', 20 | ]), 21 | Forms\Components\RichEditor::make('description'), 22 | Forms\Components\Repeater::make('features')->schema([ 23 | Forms\Components\TextInput::make('title'), 24 | Forms\Components\Textarea::make('description'), 25 | Forms\Components\TextInput::make('button_text'), 26 | Forms\Components\TextInput::make('button_link'), 27 | ]) 28 | ->collapsed() 29 | ->collapsible() 30 | ->cloneable(), 31 | Forms\Components\TextInput::make('cta_button_text'), 32 | Forms\Components\TextInput::make('cta_link'), 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | ]; 25 | 26 | /** 27 | * The attributes that should be hidden for serialization. 28 | * 29 | * @var array 30 | */ 31 | protected $hidden = [ 32 | 'password', 33 | 'remember_token', 34 | ]; 35 | 36 | /** 37 | * Get the attributes that should be cast. 38 | * 39 | * @return array 40 | */ 41 | protected function casts(): array 42 | { 43 | return [ 44 | 'email_verified_at' => 'datetime', 45 | 'password' => 'hashed', 46 | ]; 47 | } 48 | 49 | public function canAccessPanel($panel): bool 50 | { 51 | // TODO, rules for production 52 | return app()->environment('local'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function share(Request $request): array 34 | { 35 | return [ 36 | ...parent::share($request), 37 | 'auth' => [ 38 | 'user' => $request->user(), 39 | ], 40 | 'ziggy' => fn () => [ 41 | ...(new Ziggy)->toArray(), 42 | 'location' => $request->url(), 43 | ], 44 | 'globals' => Setting::globals(), 45 | 'locale' => LaravelLocalization::getCurrentLocale(), 46 | 'availableLocales' => LaravelLocalization::getSupportedLocales(), 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /resources/js/Helpers/Asset.js: -------------------------------------------------------------------------------- 1 | const recipes = { 2 | BLOG_CARD: { 3 | resize: { 4 | width: 600, 5 | height: 400, 6 | fit: 'cover' 7 | } 8 | }, 9 | BLOG: { 10 | resize: { 11 | width: 850, 12 | fit: 'contain' 13 | } 14 | }, 15 | PERSON_SMALL: { 16 | resize: { 17 | width: 100, 18 | height: 100, 19 | fit: 'cover' 20 | } 21 | }, 22 | DETAIL_SECTION: { 23 | resize: { 24 | width: 1700, 25 | height: 1134, 26 | fit: 'cover' 27 | } 28 | }, 29 | }; 30 | export default function(path, recipe = null) { 31 | if (!path) return null; 32 | if (!import.meta.env.VITE_CDN_URL) { 33 | return '/storage/' + path; 34 | } 35 | if (path.indexOf('svg') !== -1) { 36 | // SVG files cannot be transformed by the CDN, so we use the S3 bucket directly (not ideal, should be fixed in the future) 37 | return `https://${import.meta.env.VITE_AWS_BUCKET}.s3.${import.meta.env.VITE_AWS_DEFAULT_REGION}.amazonaws.com/${import.meta.env.VITE_AWS_BUCKET_ROOT}/` + path; 38 | } 39 | const edits = recipe ? recipes[recipe] : null; 40 | const properties = { 41 | key: `${import.meta.env.VITE_AWS_BUCKET_ROOT}/${path}`, 42 | edits, 43 | } 44 | const base64 = btoa(JSON.stringify(properties)); 45 | return import.meta.env.VITE_CDN_URL + base64; 46 | } 47 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | 20 | public function test_users_can_authenticate_using_the_login_screen(): void 21 | { 22 | $user = User::factory()->create(); 23 | 24 | $response = $this->post('/login', [ 25 | 'email' => $user->email, 26 | 'password' => 'password', 27 | ]); 28 | 29 | $this->assertAuthenticated(); 30 | $response->assertRedirect(route('dashboard', absolute: false)); 31 | } 32 | 33 | public function test_users_can_not_authenticate_with_invalid_password(): void 34 | { 35 | $user = User::factory()->create(); 36 | 37 | $this->post('/login', [ 38 | 'email' => $user->email, 39 | 'password' => 'wrong-password', 40 | ]); 41 | 42 | $this->assertGuest(); 43 | } 44 | 45 | public function test_users_can_logout(): void 46 | { 47 | $user = User::factory()->create(); 48 | 49 | $response = $this->actingAs($user)->post('/logout'); 50 | 51 | $this->assertGuest(); 52 | $response->assertRedirect('/'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://filament-starter.test 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | # APP_MAINTENANCE_STORE=database 14 | 15 | BCRYPT_ROUNDS=12 16 | 17 | LOG_CHANNEL=stack 18 | LOG_STACK=single 19 | LOG_DEPRECATIONS_CHANNEL=null 20 | LOG_LEVEL=debug 21 | 22 | DB_CONNECTION=sqlite 23 | # DB_HOST=127.0.0.1 24 | # DB_PORT=3306 25 | # DB_DATABASE=forge 26 | # DB_USERNAME=forge 27 | # DB_PASSWORD= 28 | 29 | SESSION_DRIVER=database 30 | SESSION_LIFETIME=120 31 | SESSION_ENCRYPT=false 32 | SESSION_PATH=/ 33 | SESSION_DOMAIN=null 34 | 35 | BROADCAST_CONNECTION=log 36 | FILESYSTEM_DISK=public 37 | FILAMENT_FILESYSTEM_DISK=public 38 | QUEUE_CONNECTION=database 39 | 40 | CACHE_STORE=database 41 | CACHE_PREFIX= 42 | 43 | MEMCACHED_HOST=127.0.0.1 44 | 45 | REDIS_CLIENT=phpredis 46 | REDIS_HOST=127.0.0.1 47 | REDIS_PASSWORD=null 48 | REDIS_PORT=6379 49 | 50 | MAIL_MAILER=log 51 | MAIL_HOST=127.0.0.1 52 | MAIL_PORT=2525 53 | MAIL_USERNAME=null 54 | MAIL_PASSWORD=null 55 | MAIL_ENCRYPTION=null 56 | MAIL_FROM_ADDRESS="hello@example.com" 57 | MAIL_FROM_NAME="${APP_NAME}" 58 | 59 | AWS_ACCESS_KEY_ID= 60 | AWS_SECRET_ACCESS_KEY= 61 | AWS_DEFAULT_REGION=us-east-1 62 | AWS_BUCKET= 63 | AWS_USE_PATH_STYLE_ENDPOINT=false 64 | AWS_BUCKET_ROOT=images 65 | 66 | VITE_APP_NAME="${APP_NAME}" 67 | 68 | #VITE_AWS_BUCKET="${AWS_BUCKET}" 69 | #VITE_AWS_BUCKET_ROOT="${AWS_BUCKET_ROOT}" 70 | #VITE_CDN_URL=... 71 | -------------------------------------------------------------------------------- /data/helloworld.json: -------------------------------------------------------------------------------- 1 | {"en":[{"type":"rich_editor","data":{"content":"

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sagittis, metus non fermentum ornare, felis lectus convallis leo, vulputate fermentum enim nisl sed erat. Sed nisi quam, maximus eu dolor at, imperdiet accumsan velit.

Vivamus id sem eleifend, tempor neque sit amet, cursus dui. Pellentesque tincidunt est id scelerisque commodo. Mauris malesuada tortor vel ex sagittis, vitae varius dolor auctor. Mauris dignissim bibendum tortor, id molestie libero malesuada nec. Mauris eget elit vitae ex sollicitudin rutrum. Curabitur et risus nisl. Vestibulum egestas lorem vel sem elementum, pharetra dignissim tortor hendrerit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae;

"}}],"fr":[{"type":"rich_editor","data":{"content":"

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sagittis, metus non fermentum ornare, felis lectus convallis leo, vulputate fermentum enim nisl sed erat. Sed nisi quam, maximus eu dolor at, imperdiet accumsan velit.

Vivamus id sem eleifend, tempor neque sit amet, cursus dui. Pellentesque tincidunt est id scelerisque commodo. Mauris malesuada tortor vel ex sagittis, vitae varius dolor auctor. Mauris dignissim bibendum tortor, id molestie libero malesuada nec. Mauris eget elit vitae ex sollicitudin rutrum. Curabitur et risus nisl. Vestibulum egestas lorem vel sem elementum, pharetra dignissim tortor hendrerit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae;

"}}]} 2 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/StatsSection.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordUpdateTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | $response = $this 19 | ->actingAs($user) 20 | ->from('/profile') 21 | ->put('/password', [ 22 | 'current_password' => 'password', 23 | 'password' => 'new-password', 24 | 'password_confirmation' => 'new-password', 25 | ]); 26 | 27 | $response 28 | ->assertSessionHasNoErrors() 29 | ->assertRedirect('/profile'); 30 | 31 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 32 | } 33 | 34 | public function test_correct_password_must_be_provided_to_update_password(): void 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this 39 | ->actingAs($user) 40 | ->from('/profile') 41 | ->put('/password', [ 42 | 'current_password' => 'wrong-password', 43 | 'password' => 'new-password', 44 | 'password_confirmation' => 'new-password', 45 | ]); 46 | 47 | $response 48 | ->assertSessionHasErrors('current_password') 49 | ->assertRedirect('/profile'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withExceptions(function (Exceptions $exceptions) { 12 | $exceptions->respond(function (Response $response, Throwable $exception, Request $request) { 13 | if (! app()->environment(['local', 'testing']) && in_array($response->getStatusCode(), [500, 503, 404, 403])) { 14 | return Inertia::render('Error', ['status' => $response->getStatusCode()]) 15 | ->toResponse($request) 16 | ->setStatusCode($response->getStatusCode()); 17 | } elseif ($response->getStatusCode() === 419) { 18 | return back()->with([ 19 | 'message' => 'The page expired, please try again.', 20 | ]); 21 | } 22 | 23 | return $response; 24 | }); 25 | }) 26 | ->withRouting( 27 | web: __DIR__.'/../routes/web.php', 28 | commands: __DIR__.'/../routes/console.php', 29 | health: '/up', 30 | ) 31 | ->withMiddleware(function (Middleware $middleware) { 32 | $middleware->web(append: [ 33 | \App\Http\Middleware\HandleInertiaRequests::class, 34 | \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, 35 | ]); 36 | 37 | // 38 | }) 39 | ->withExceptions(function (Exceptions $exceptions) { 40 | // 41 | })->create(); 42 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/FeaturelistSection.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /config/sitemap.php: -------------------------------------------------------------------------------- 1 | [ 15 | 16 | /* 17 | * Whether or not cookies are used in a request. 18 | */ 19 | RequestOptions::COOKIES => true, 20 | 21 | /* 22 | * The number of seconds to wait while trying to connect to a server. 23 | * Use 0 to wait indefinitely. 24 | */ 25 | RequestOptions::CONNECT_TIMEOUT => 10, 26 | 27 | /* 28 | * The timeout of the request in seconds. Use 0 to wait indefinitely. 29 | */ 30 | RequestOptions::TIMEOUT => 10, 31 | 32 | /* 33 | * Describes the redirect behavior of a request. 34 | */ 35 | RequestOptions::ALLOW_REDIRECTS => false, 36 | ], 37 | 38 | /* 39 | * The sitemap generator can execute JavaScript on each page so it will 40 | * discover links that are generated by your JS scripts. This feature 41 | * is powered by headless Chrome. 42 | */ 43 | 'execute_javascript' => false, 44 | 45 | /* 46 | * The package will make an educated guess as to where Google Chrome is installed. 47 | * You can also manually pass its location here. 48 | */ 49 | 'chrome_binary_path' => null, 50 | 51 | /* 52 | * The sitemap generator uses a CrawlProfile implementation to determine 53 | * which urls should be crawled for the sitemap. 54 | */ 55 | 'crawl_profile' => Profile::class, 56 | 57 | ]; 58 | -------------------------------------------------------------------------------- /app/Http/CacheProfiles/InertiaAwareCacheProfile.php: -------------------------------------------------------------------------------- 1 | inertia() ? 'inertia' : 'no-inertia'; 15 | } 16 | 17 | public function shouldCacheRequest(Request $request): bool 18 | { 19 | if ($this->isRunningInConsole()) { 20 | return false; 21 | } 22 | 23 | return $request->isMethod('get'); 24 | } 25 | 26 | public function shouldCacheResponse(Response $response): bool 27 | { 28 | if (! $this->hasCacheableResponseCode($response)) { 29 | return false; 30 | } 31 | 32 | if (! $this->hasCacheableContentType($response)) { 33 | return false; 34 | } 35 | 36 | return true; 37 | } 38 | 39 | public function hasCacheableResponseCode(Response $response): bool 40 | { 41 | if ($response->isSuccessful()) { 42 | return true; 43 | } 44 | 45 | if ($response->isRedirection()) { 46 | return true; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | public function hasCacheableContentType(Response $response): bool 53 | { 54 | $contentType = $response->headers->get('Content-Type', ''); 55 | 56 | if (str_starts_with($contentType, 'text/')) { 57 | return true; 58 | } 59 | 60 | if (Str::contains($contentType, ['/json', '+json'])) { 61 | return true; 62 | } 63 | 64 | return false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/FaqSection.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 38 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | unverified()->create(); 19 | 20 | $response = $this->actingAs($user)->get('/verify-email'); 21 | 22 | $response->assertStatus(200); 23 | } 24 | 25 | public function test_email_can_be_verified(): void 26 | { 27 | $user = User::factory()->unverified()->create(); 28 | 29 | Event::fake(); 30 | 31 | $verificationUrl = URL::temporarySignedRoute( 32 | 'verification.verify', 33 | now()->addMinutes(60), 34 | ['id' => $user->id, 'hash' => sha1($user->email)] 35 | ); 36 | 37 | $response = $this->actingAs($user)->get($verificationUrl); 38 | 39 | Event::assertDispatched(Verified::class); 40 | $this->assertTrue($user->fresh()->hasVerifiedEmail()); 41 | $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 42 | } 43 | 44 | public function test_email_is_not_verified_with_invalid_hash(): void 45 | { 46 | $user = User::factory()->unverified()->create(); 47 | 48 | $verificationUrl = URL::temporarySignedRoute( 49 | 'verification.verify', 50 | now()->addMinutes(60), 51 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 52 | ); 53 | 54 | $this->actingAs($user)->get($verificationUrl); 55 | 56 | $this->assertFalse($user->fresh()->hasVerifiedEmail()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /data/home.json: -------------------------------------------------------------------------------- 1 | {"en":[{"type":"title_section","data":{"title":"Filament Marketing Starter Kit","subtitle":"Kickstart your next marketing page","variant":"default","description":null,"buttons":[{"label":"Github","url":"https://github.com/sabatinomasala/filament-marketing-starter","variant":"red"}]}},{"type":"featurelist_section","data":{"title":"Filament Marketing Starter Kit","subtitle":"Kickstart your next marketing page","content":null,"features":[{"title":"Vue blocks","description":"

All blocks are built using Vue 3 and the composition API

"},{"title":"Tailwind","description":"

Tailwind makes styling the blocks easy

"},{"title":"Inertia SSR support","description":"

Vite has been configured for generating SSR bundles, so the marketing website is usable without Javascript

"},{"title":"ShadCN components","description":"

ShadCN-vue has been configured so adding components is as easy as running a single command

"}]}},{"type":"faq_section","data":{"title":"Question & Answer","subtitle":null,"questions":[{"question":"Does this template support multi-language?","answer":"

Yes! Using a combination of mcamara/laravel-localization and spatie/laravel-translatable the pages can be translated

"},{"question":"Does this template support SSR?","answer":"

Yes, just execute:

yarn build\nphp artisan inertia:start-ssr
"}]}}],"fr":[{"type":"featurelist_section","data":{"title":"Filament Marketing Starter Kit","subtitle":"Kickstart your next marketing page","content":null,"features":[{"title":"Vue blocks","description":"

All blocks are built using Vue 3 and the composition API

"},{"title":"Tailwind","description":"

Tailwind makes styling the blocks easy

"},{"title":"Inertia SSR support","description":"

Vite has been configured for generating SSR bundles, so the marketing website is usable without Javascript

"},{"title":"ShadCN components","description":"

ShadCN-vue has been configured so adding components is as easy as running a single command

"}]}}]} 2 | -------------------------------------------------------------------------------- /config/inertia.php: -------------------------------------------------------------------------------- 1 | [ 23 | 24 | 'enabled' => true, 25 | 26 | 'url' => 'http://127.0.0.1:13715', 27 | 28 | // 'bundle' => base_path('bootstrap/ssr/ssr.mjs'), 29 | 30 | ], 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Testing 35 | |-------------------------------------------------------------------------- 36 | | 37 | | The values described here are used to locate Inertia components on the 38 | | filesystem. For instance, when using `assertInertia`, the assertion 39 | | attempts to locate the component as a file relative to any of the 40 | | paths AND with any of the extensions specified here. 41 | | 42 | */ 43 | 44 | 'testing' => [ 45 | 46 | 'ensure_pages_exist' => true, 47 | 48 | 'page_paths' => [ 49 | 50 | resource_path('js/Pages'), 51 | 52 | ], 53 | 54 | 'page_extensions' => [ 55 | 56 | 'js', 57 | 'jsx', 58 | 'svelte', 59 | 'ts', 60 | 'tsx', 61 | 'vue', 62 | 63 | ], 64 | 65 | ], 66 | 67 | ]; 68 | -------------------------------------------------------------------------------- /app/Console/Commands/EnsureDefaultSettings.php: -------------------------------------------------------------------------------- 1 | each(function($group, $groupKey) use ($locale) { 32 | collect($group)->each(function($value, $key) use ($locale, $groupKey) { 33 | $type = 'string'; 34 | if (isset($value['type'])) { 35 | $type = $value['type']; 36 | } 37 | if (isset($value['data'])) { 38 | $value = $value['data']; 39 | } 40 | if (Setting::where('key', $key)->where('group', $groupKey)->exists()) { 41 | return; 42 | } 43 | $setting = Setting::create([ 44 | 'key' => $key, 45 | 'group' => $groupKey, 46 | 'value' => [ 47 | $locale => [ 48 | [ 49 | 'type' => $type, 50 | 'data' => [ 51 | 'value' => $value 52 | ] 53 | ] 54 | ] 55 | ] 56 | ]); 57 | $setting->save(); 58 | }); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /resources/js/Pages/Error.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /app/Filament/Resources/PageResource.php: -------------------------------------------------------------------------------- 1 | schema([ 28 | ...Properties::make($form), 29 | RichContent::builder($form)->columnSpanFull(), 30 | ]); 31 | } 32 | 33 | public static function table(Table $table): Table 34 | { 35 | return $table 36 | ->columns([ 37 | TextColumn::make('title') 38 | ]) 39 | ->filters([ 40 | // 41 | ]) 42 | ->actions([ 43 | Tables\Actions\EditAction::make(), 44 | ]) 45 | ->bulkActions([ 46 | Tables\Actions\BulkActionGroup::make([ 47 | Tables\Actions\DeleteBulkAction::make(), 48 | ]), 49 | ]); 50 | } 51 | 52 | public static function getRelations(): array 53 | { 54 | return [ 55 | // 56 | ]; 57 | } 58 | 59 | public static function getPages(): array 60 | { 61 | return [ 62 | 'index' => Pages\ListPages::route('/'), 63 | 'create' => Pages\CreatePage::route('/create'), 64 | 'edit' => Pages\EditPage::route('/{record}/edit'), 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/HeroSection.vue: -------------------------------------------------------------------------------- 1 | 24 | 34 | -------------------------------------------------------------------------------- /database/migrations/2024_08_02_084106_ensure_default_settings_and_data.php: -------------------------------------------------------------------------------- 1 | insert([ 16 | [ 17 | 'title' => '{"en": "Home", "fr": "Home"}', 18 | 'slug' => '{"en": "home", "fr": "home"}', 19 | 'content' => file_get_contents(base_path('data/home.json')), 20 | 'created_at' => now()->timestamp, 21 | 'updated_at' => now()->timestamp, 22 | ], 23 | [ 24 | 'title' => '{"en": "Blog", "fr": "Blog"}', 25 | 'slug' => '{"en": "blog", "fr": "blog"}', 26 | 'content' => file_get_contents(base_path('data/blog.json')), 27 | 'created_at' => now()->timestamp, 28 | 'updated_at' => now()->timestamp, 29 | ] 30 | ]); 31 | DB::table('posts')->insert([ 32 | [ 33 | 'title' => '{"en": "Hello World!", "fr": "Bonjour Monde!"}', 34 | 'slug' => '{"en": "hello-world", "fr": "bonjour-monde"}', 35 | 'published_at' => now()->startOfDay()->startOfYear()->timestamp, 36 | 'short_description' => '{"en": "This is an example article", "fr": "Ceci est un exemple d\'article"}', 37 | 'content' => file_get_contents(base_path('data/helloworld.json')), 38 | 'created_at' => now()->timestamp, 39 | 'updated_at' => now()->timestamp, 40 | ] 41 | ]); 42 | } 43 | 44 | /** 45 | * Reverse the migrations. 46 | */ 47 | public function down(): void 48 | { 49 | \App\Models\Setting::all()->each->delete(); 50 | \App\Models\Page::all()->each->delete(); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /resources/js/Components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 16 | 41 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/TitleSection.vue: -------------------------------------------------------------------------------- 1 | 29 | 45 | 52 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_reset_password_link_can_be_requested(): void 23 | { 24 | Notification::fake(); 25 | 26 | $user = User::factory()->create(); 27 | 28 | $this->post('/forgot-password', ['email' => $user->email]); 29 | 30 | Notification::assertSentTo($user, ResetPassword::class); 31 | } 32 | 33 | public function test_reset_password_screen_can_be_rendered(): void 34 | { 35 | Notification::fake(); 36 | 37 | $user = User::factory()->create(); 38 | 39 | $this->post('/forgot-password', ['email' => $user->email]); 40 | 41 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 42 | $response = $this->get('/reset-password/'.$notification->token); 43 | 44 | $response->assertStatus(200); 45 | 46 | return true; 47 | }); 48 | } 49 | 50 | public function test_password_can_be_reset_with_valid_token(): void 51 | { 52 | Notification::fake(); 53 | 54 | $user = User::factory()->create(); 55 | 56 | $this->post('/forgot-password', ['email' => $user->email]); 57 | 58 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 59 | $response = $this->post('/reset-password', [ 60 | 'token' => $notification->token, 61 | 'email' => $user->email, 62 | 'password' => 'password', 63 | 'password_confirmation' => 'password', 64 | ]); 65 | 66 | $response 67 | ->assertSessionHasNoErrors() 68 | ->assertRedirect(route('login')); 69 | 70 | return true; 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Filament/Resources/PostResource.php: -------------------------------------------------------------------------------- 1 | schema([ 30 | ...Properties::make($form, ['title', 'slug', 'seo', 'published_at', 'featured_image', 'short_description']), 31 | RichContent::builder($form)->columnSpanFull(), 32 | Hidden::make('is_slug_changed_manually') 33 | ->default(false) 34 | ->dehydrated(false), 35 | ]); 36 | } 37 | 38 | public static function table(Table $table): Table 39 | { 40 | return $table 41 | ->columns([ 42 | TextColumn::make('title'), 43 | TextColumn::make('published_at')->dateTime()->sortable() 44 | ]) 45 | ->filters([ 46 | // 47 | ]) 48 | ->actions([ 49 | Tables\Actions\EditAction::make(), 50 | ]) 51 | ->bulkActions([ 52 | Tables\Actions\BulkActionGroup::make([ 53 | Tables\Actions\DeleteBulkAction::make(), 54 | ]), 55 | ]) 56 | ->defaultSort('published_at', 'desc'); 57 | } 58 | 59 | public static function getRelations(): array 60 | { 61 | return [ 62 | // 63 | ]; 64 | } 65 | 66 | public static function getPages(): array 67 | { 68 | return [ 69 | 'index' => Pages\ListPosts::route('/'), 70 | 'create' => Pages\CreatePost::route('/create'), 71 | 'edit' => Pages\EditPost::route('/{record}/edit'), 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/DialogScrollContent.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 76 | -------------------------------------------------------------------------------- /app/Providers/Filament/AdminPanelProvider.php: -------------------------------------------------------------------------------- 1 | default() 28 | ->id('admin') 29 | ->path('admin') 30 | ->login() 31 | ->colors([ 32 | 'primary' => Color::Amber, 33 | ]) 34 | ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') 35 | ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') 36 | ->pages([ 37 | Pages\Dashboard::class, 38 | ]) 39 | ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') 40 | ->widgets([ 41 | Widgets\AccountWidget::class, 42 | Widgets\FilamentInfoWidget::class, 43 | ]) 44 | ->middleware([ 45 | EncryptCookies::class, 46 | AddQueuedCookiesToResponse::class, 47 | StartSession::class, 48 | AuthenticateSession::class, 49 | ShareErrorsFromSession::class, 50 | VerifyCsrfToken::class, 51 | SubstituteBindings::class, 52 | DisableBladeIconComponents::class, 53 | DispatchServingFilamentEvent::class, 54 | ]) 55 | ->authMiddleware([ 56 | Authenticate::class, 57 | ]) 58 | ->plugin(SpatieLaravelTranslatablePlugin::make() 59 | ->defaultLocales(['en', 'fr'])); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /resources/js/Pages/Blog/Show.vue: -------------------------------------------------------------------------------- 1 | 34 | 51 | -------------------------------------------------------------------------------- /resources/js/Components/ui/dialog/DialogContent.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 64 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: hsl(252, 33%, 97%); 8 | --foreground: hsl(222.2 84% 4.9%); 9 | 10 | --muted: hsl(210 40% 96.1%); 11 | --muted-foreground: hsl(215.4 16.3% 46.9%); 12 | 13 | --popover: hsl(0 0% 100%); 14 | --popover-foreground: hsl(222.2 84% 4.9%); 15 | 16 | --card: hsl(0 0% 100%); 17 | --card-foreground: hsl(222.2 84% 4.9%); 18 | 19 | --border: hsl(214.3 31.8% 91.4%); 20 | --input: hsl(214.3 31.8% 91.4%); 21 | 22 | --primary: hsl(248, 67%, 12%); 23 | --primary-foreground: hsl(0, 0%, 100%); 24 | 25 | --secondary: hsl(293, 80%, 10%); 26 | --secondary-foreground: hsl(0, 0%, 100%); 27 | 28 | --accent: hsl(354, 68%, 50%); 29 | --accent-foreground: hsl(222.2 47.4% 11.2%); 30 | 31 | --destructive: hsl(0 84.2% 60.2%); 32 | --destructive-foreground: hsl(210 40% 98%); 33 | 34 | --ring: hsl(222.2 84% 4.9%); 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: hsl(222.2 84% 4.9%); 41 | --foreground: hsl(210 40% 98%); 42 | 43 | --muted: hsl(217.2 32.6% 17.5%); 44 | --muted-foreground: hsl(215 20.2% 65.1%); 45 | 46 | --popover: hsl(222.2 84% 4.9%); 47 | --popover-foreground: hsl(210 40% 98%); 48 | 49 | --card: hsl(222.2 84% 4.9%); 50 | --card-foreground: hsl(210 40% 98%); 51 | 52 | --border: hsl(217.2 32.6% 17.5%); 53 | --input: hsl(217.2 32.6% 17.5%); 54 | 55 | --primary: hsl(210 40% 98%); 56 | --primary-foreground: hsl(222.2 47.4% 11.2%); 57 | 58 | --secondary: hsl(217.2 32.6% 17.5%); 59 | --secondary-foreground: hsl(210 40% 98%); 60 | 61 | --accent: hsl(217.2 32.6% 17.5%); 62 | --accent-foreground: hsl(210 40% 98%); 63 | 64 | --destructive: hsl(0 62.8% 30.6%); 65 | --destructive-foreground: hsl(210 40% 98%); 66 | 67 | --ring: hsl(212.7 26.8% 83.9%); 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | 80 | body { 81 | font-family: Helvetica, Arial, sans-serif; 82 | } 83 | 84 | .font-secondary { 85 | font-family: 'Open Sans', sans-serif; 86 | } 87 | 88 | pre { 89 | display: block; 90 | font-family: monospace; 91 | white-space: pre; 92 | padding: 1em; 93 | border-radius: 5px; 94 | @apply bg-muted text-muted-foreground; 95 | } 96 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 15 | Route::get('register', [RegisteredUserController::class, 'create']) 16 | ->name('register'); 17 | 18 | Route::post('register', [RegisteredUserController::class, 'store']); 19 | 20 | Route::get('login', [AuthenticatedSessionController::class, 'create']) 21 | ->name('login'); 22 | 23 | Route::post('login', [AuthenticatedSessionController::class, 'store']); 24 | 25 | Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) 26 | ->name('password.request'); 27 | 28 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) 29 | ->name('password.email'); 30 | 31 | Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) 32 | ->name('password.reset'); 33 | 34 | Route::post('reset-password', [NewPasswordController::class, 'store']) 35 | ->name('password.store'); 36 | }); 37 | 38 | Route::middleware('auth')->group(function () { 39 | Route::get('verify-email', EmailVerificationPromptController::class) 40 | ->name('verification.notice'); 41 | 42 | Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) 43 | ->middleware(['signed', 'throttle:6,1']) 44 | ->name('verification.verify'); 45 | 46 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 47 | ->middleware('throttle:6,1') 48 | ->name('verification.send'); 49 | 50 | Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) 51 | ->name('password.confirm'); 52 | 53 | Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); 54 | 55 | Route::put('password', [PasswordController::class, 'update'])->name('password.update'); 56 | 57 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 58 | ->name('logout'); 59 | }); 60 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | 'throw' => false, 37 | ], 38 | 39 | 'public' => [ 40 | 'driver' => 'local', 41 | 'root' => storage_path('app/public'), 42 | 'url' => env('APP_URL').'/storage', 43 | 'visibility' => 'public', 44 | 'throw' => false, 45 | ], 46 | 47 | 's3' => [ 48 | 'driver' => 's3', 49 | 'root' => env('AWS_BUCKET_ROOT'), 50 | 'key' => env('AWS_ACCESS_KEY_ID'), 51 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 52 | 'region' => env('AWS_DEFAULT_REGION'), 53 | 'bucket' => env('AWS_BUCKET'), 54 | 'url' => env('AWS_URL'), 55 | 'endpoint' => env('AWS_ENDPOINT'), 56 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 57 | 'throw' => false, 58 | ], 59 | 60 | ], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Symbolic Links 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Here you may configure the symbolic links that will be created when the 68 | | `storage:link` Artisan command is executed. The array keys should be 69 | | the locations of the links and the values should be their targets. 70 | | 71 | */ 72 | 73 | 'links' => [ 74 | public_path('storage') => storage_path('app/public'), 75 | ], 76 | 77 | ]; 78 | -------------------------------------------------------------------------------- /app/Http/Controllers/PageController.php: -------------------------------------------------------------------------------- 1 | ' . app()->currentLocale(), $slug)->firstOrFail(); 20 | $res = $this->render($page); 21 | return $res; 22 | } 23 | 24 | protected function render($page) 25 | { 26 | $pageData = PageResource::make($page)->toArray(request()); 27 | $pageData['content'] = collect($page['content'])->map(function($content) { 28 | if ($content['type'] === 'blog_section') { 29 | $posts = Post::orderBy('published_at', 'desc')->paginate($content['data']['per_page']); 30 | $content['data'] = [ 31 | 'items' => PostResource::collection($posts->items())->toArray(request()), 32 | 'meta' => [ 33 | 'base_path' => '/' . request()->path(), 34 | 'current_page' => $posts->currentPage(), 35 | 'first_page_url' => $posts->url(1), 36 | 'from' => $posts->firstItem(), 37 | 'last_page' => $posts->lastPage(), 38 | 'last_page_url' => $posts->url($posts->lastPage()), 39 | 'links' => $posts->linkCollection()->toArray(), 40 | 'next_page_url' => $posts->nextPageUrl(), 41 | 'path' => $posts->path(), 42 | 'per_page' => $posts->perPage(), 43 | 'prev_page_url' => $posts->previousPageUrl(), 44 | 'to' => $posts->lastItem(), 45 | 'total' => $posts->total(), 46 | ] 47 | ]; 48 | } else if ($content['type'] === 'modules_section') { 49 | $modules = Module::orderBy('sort_order', 'desc')->get(); 50 | $content['data'] = [ 51 | 'items' => ModuleResource::collection($modules)->toArray(request()), 52 | 'meta' => [ 53 | 'base_path' => '/' . request()->path(), 54 | ] 55 | ]; 56 | } 57 | return $content; 58 | })->toArray(); 59 | return Inertia::render('Page', [ 60 | 'page' => $pageData, 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Filament/BlockGroups/Properties.php: -------------------------------------------------------------------------------- 1 | afterStateUpdated(function ($get, $set, ?string $state) { 22 | if (! $get('meta.is_slug_changed_manually') && filled($state)) { 23 | $set('slug', Str::slug($state)); 24 | } 25 | if (! $get('is_seo_title_changed_manually') && filled($state)) { 26 | $set('seo.title', $state); 27 | } 28 | }) 29 | ->reactive() 30 | ->live(onBlur: false, debounce: 500) 31 | ->required(), 32 | TextInput::make('slug') 33 | ->afterStateUpdated(function ($set) { 34 | $set('meta.is_slug_changed_manually', true); 35 | }) 36 | ->reactive() 37 | ->required(), 38 | Hidden::make('meta.is_slug_changed_manually'), 39 | Hidden::make('meta.is_seo_title_changed_manually'), 40 | ]; 41 | if (in_array('published_at', $fields)) { 42 | $schema[] = DateTimePicker::make('published_at'); 43 | } 44 | if (in_array('featured_image', $fields)) { 45 | $schema[] = FileUpload::make('featured_image')->image(); 46 | } 47 | if (in_array('short_description', $fields)) { 48 | $schema[] = TextArea::make('short_description')->required()->columnSpanFull(); 49 | } 50 | if (in_array('seo', $fields)) { 51 | $schema[] = Section::make('SEO') 52 | ->schema([ 53 | Forms\Components\TextInput::make('seo.title') 54 | ->afterStateUpdated(function ($set) { 55 | $set('meta.is_seo_title_changed_manually', true); 56 | }) 57 | ->reactive(), 58 | Forms\Components\Textarea::make('seo.description'), 59 | FileUpload::make('seo.image')->image(), 60 | ]) 61 | ->collapsed(); 62 | } 63 | return [ 64 | Section::make('Properties') 65 | ->schema($schema) 66 | ->columns(2) 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Feature/ProfileTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $response = $this 18 | ->actingAs($user) 19 | ->get('/profile'); 20 | 21 | $response->assertOk(); 22 | } 23 | 24 | public function test_profile_information_can_be_updated(): void 25 | { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this 29 | ->actingAs($user) 30 | ->patch('/profile', [ 31 | 'name' => 'Test User', 32 | 'email' => 'test@example.com', 33 | ]); 34 | 35 | $response 36 | ->assertSessionHasNoErrors() 37 | ->assertRedirect('/profile'); 38 | 39 | $user->refresh(); 40 | 41 | $this->assertSame('Test User', $user->name); 42 | $this->assertSame('test@example.com', $user->email); 43 | $this->assertNull($user->email_verified_at); 44 | } 45 | 46 | public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void 47 | { 48 | $user = User::factory()->create(); 49 | 50 | $response = $this 51 | ->actingAs($user) 52 | ->patch('/profile', [ 53 | 'name' => 'Test User', 54 | 'email' => $user->email, 55 | ]); 56 | 57 | $response 58 | ->assertSessionHasNoErrors() 59 | ->assertRedirect('/profile'); 60 | 61 | $this->assertNotNull($user->refresh()->email_verified_at); 62 | } 63 | 64 | public function test_user_can_delete_their_account(): void 65 | { 66 | $user = User::factory()->create(); 67 | 68 | $response = $this 69 | ->actingAs($user) 70 | ->delete('/profile', [ 71 | 'password' => 'password', 72 | ]); 73 | 74 | $response 75 | ->assertSessionHasNoErrors() 76 | ->assertRedirect('/'); 77 | 78 | $this->assertGuest(); 79 | $this->assertNull($user->fresh()); 80 | } 81 | 82 | public function test_correct_password_must_be_provided_to_delete_account(): void 83 | { 84 | $user = User::factory()->create(); 85 | 86 | $response = $this 87 | ->actingAs($user) 88 | ->from('/profile') 89 | ->delete('/profile', [ 90 | 'password' => 'wrong-password', 91 | ]); 92 | 93 | $response 94 | ->assertSessionHasErrors('password') 95 | ->assertRedirect('/profile'); 96 | 97 | $this->assertNotNull($user->fresh()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The skeleton application for the Laravel framework.", 5 | "keywords": ["laravel", "framework"], 6 | "license": "MIT", 7 | "require": { 8 | "php": "^8.2", 9 | "filament/filament": "^3.2", 10 | "filament/spatie-laravel-translatable-plugin": "^3.2", 11 | "inertiajs/inertia-laravel": "^1.0", 12 | "laravel/framework": "^11.9", 13 | "laravel/sanctum": "^4.0", 14 | "laravel/tinker": "^2.9", 15 | "league/flysystem-aws-s3-v3": "^3.0", 16 | "mcamara/laravel-localization": "^2.0", 17 | "spatie/laravel-responsecache": "^7.5", 18 | "spatie/laravel-sitemap": "^7.2", 19 | "spatie/laravel-translatable": "^6.7", 20 | "tightenco/ziggy": "^2.0" 21 | }, 22 | "require-dev": { 23 | "barryvdh/laravel-debugbar": "^3.13", 24 | "barryvdh/laravel-ide-helper": "^3.1", 25 | "fakerphp/faker": "^1.23", 26 | "laravel/breeze": "^2.1", 27 | "laravel/pint": "^1.13", 28 | "laravel/sail": "^1.26", 29 | "mockery/mockery": "^1.6", 30 | "nunomaduro/collision": "^8.0", 31 | "phpunit/phpunit": "^11.0.1" 32 | }, 33 | "repositories": [ 34 | { 35 | "type": "github", 36 | "url": "https://github.com/lara-zeus/translatable" 37 | } 38 | ], 39 | "autoload": { 40 | "files": ["app/functions.php"], 41 | "psr-4": { 42 | "App\\": "app/", 43 | "Database\\Factories\\": "database/factories/", 44 | "Database\\Seeders\\": "database/seeders/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Tests\\": "tests/" 50 | } 51 | }, 52 | "scripts": { 53 | "post-autoload-dump": [ 54 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 55 | "@php artisan package:discover --ansi", 56 | "@php artisan filament:upgrade" 57 | ], 58 | "post-update-cmd": [ 59 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 60 | ], 61 | "post-root-package-install": [ 62 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 63 | ], 64 | "post-create-project-cmd": [ 65 | "@php artisan key:generate --ansi", 66 | "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", 67 | "@php artisan migrate --graceful --ansi" 68 | ] 69 | }, 70 | "extra": { 71 | "laravel": { 72 | "dont-discover": [] 73 | } 74 | }, 75 | "config": { 76 | "optimize-autoloader": true, 77 | "preferred-install": "dist", 78 | "sort-packages": true, 79 | "allow-plugins": { 80 | "pestphp/pest-plugin": true, 81 | "php-http/discovery": true 82 | } 83 | }, 84 | "minimum-stability": "dev", 85 | "prefer-stable": true 86 | } 87 | -------------------------------------------------------------------------------- /public/css/filament/support/support.css: -------------------------------------------------------------------------------- 1 | .fi-pagination-items,.fi-pagination-overview,.fi-pagination-records-per-page-select:not(.fi-compact){display:none}@supports (container-type:inline-size){.fi-pagination{container-type:inline-size}@container (min-width: 28rem){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@container (min-width: 56rem){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}@supports not (container-type:inline-size){@media (min-width:640px){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@media (min-width:768px){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{background-color:#333;border-radius:4px;color:#fff;font-size:14px;line-height:1.4;outline:0;position:relative;transition-property:transform,visibility,opacity;white-space:normal}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-top-color:initial;border-width:8px 8px 0;bottom:-7px;left:0;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:initial;border-width:0 8px 8px;left:0;top:-7px;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-left-color:initial;border-width:8px 0 8px 8px;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{border-right-color:initial;border-width:8px 8px 8px 0;left:-7px;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{color:#333;height:16px;width:16px}.tippy-arrow:before{border-color:transparent;border-style:solid;content:"";position:absolute}.tippy-content{padding:5px 9px;position:relative;z-index:1}.tippy-box[data-theme~=light]{background-color:#fff;box-shadow:0 0 20px 4px #9aa1b126,0 4px 80px -8px #24282f40,0 4px 4px -2px #5b5e6926;color:#26323d}.tippy-box[data-theme~=light][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=light][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff}.tippy-box[data-theme~=light][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=light][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff}.tippy-box[data-theme~=light]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=light]>.tippy-svg-arrow{fill:#fff}.fi-sortable-ghost{opacity:.3} -------------------------------------------------------------------------------- /public/js/filament/tables/components/table.js: -------------------------------------------------------------------------------- 1 | function n(){return{collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){let e=this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[];for(let t of e)t.removeEventListener("click",this.handleCheckboxClick),t.addEventListener("click",s=>this.handleCheckboxClick(s,t))},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let o=s.indexOf(this.lastChecked),r=s.indexOf(t),l=[o,r].sort((i,d)=>i-d),c=[];for(let i=l[0];i<=l[1];i++)s[i].checked=t.checked,c.push(s[i].value);t.checked?this.selectRecords(c):this.deselectRecords(c)}this.lastChecked=t}}}export{n as default}; 2 | -------------------------------------------------------------------------------- /resources/views/vendor/livewire/simple-bootstrap.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | if (! isset($scrollTo)) { 3 | $scrollTo = 'body'; 4 | } 5 | 6 | $scrollIntoViewJsSnippet = ($scrollTo !== false) 7 | ? << 14 | @if ($paginator->hasPages()) 15 | 52 | @endif 53 | 54 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/DetailSection.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | 60 | -------------------------------------------------------------------------------- /config/settings.php: -------------------------------------------------------------------------------- 1 | [ 10 | 11 | ], 12 | 13 | /* 14 | * The path where the settings classes will be created. 15 | */ 16 | 'setting_class_path' => app_path('Settings'), 17 | 18 | /* 19 | * In these directories settings migrations will be stored and ran when migrating. A settings 20 | * migration created via the make:settings-migration command will be stored in the first path or 21 | * a custom defined path when running the command. 22 | */ 23 | 'migrations_paths' => [ 24 | database_path('settings'), 25 | ], 26 | 27 | /* 28 | * When no repository was set for a settings class the following repository 29 | * will be used for loading and saving settings. 30 | */ 31 | 'default_repository' => 'database', 32 | 33 | /* 34 | * Settings will be stored and loaded from these repositories. 35 | */ 36 | 'repositories' => [ 37 | 'database' => [ 38 | 'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class, 39 | 'model' => null, 40 | 'table' => null, 41 | 'connection' => null, 42 | ], 43 | 'redis' => [ 44 | 'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class, 45 | 'connection' => null, 46 | 'prefix' => null, 47 | ], 48 | ], 49 | 50 | /* 51 | * The encoder and decoder will determine how settings are stored and 52 | * retrieved in the database. By default, `json_encode` and `json_decode` 53 | * are used. 54 | */ 55 | 'encoder' => null, 56 | 'decoder' => null, 57 | 58 | /* 59 | * The contents of settings classes can be cached through your application, 60 | * settings will be stored within a provided Laravel store and can have an 61 | * additional prefix. 62 | */ 63 | 'cache' => [ 64 | 'enabled' => env('SETTINGS_CACHE_ENABLED', false), 65 | 'store' => null, 66 | 'prefix' => null, 67 | 'ttl' => null, 68 | ], 69 | 70 | /* 71 | * These global casts will be automatically used whenever a property within 72 | * your settings class isn't a default PHP type. 73 | */ 74 | 'global_casts' => [ 75 | DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class, 76 | DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class, 77 | // Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class, 78 | Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class, 79 | ], 80 | 81 | /* 82 | * The package will look for settings in these paths and automatically 83 | * register them. 84 | */ 85 | 'auto_discover_settings' => [ 86 | app_path('Settings'), 87 | ], 88 | 89 | /* 90 | * Automatically discovered settings classes can be cached, so they don't 91 | * need to be searched each time the application boots up. 92 | */ 93 | 'discovered_settings_cache_path' => base_path('bootstrap/cache'), 94 | ]; 95 | -------------------------------------------------------------------------------- /resources/js/Components/blocks/BlogSection.vue: -------------------------------------------------------------------------------- 1 | 45 | 54 | -------------------------------------------------------------------------------- /app/Models/Setting.php: -------------------------------------------------------------------------------- 1 | map(function($setting) { 23 | return [ 24 | 'group' => $setting->group, 25 | 'key' => $setting->key, 26 | 'parsed' => $setting->parsed(), 27 | ]; 28 | }) 29 | ->groupBy('group') 30 | ->each(function($group, $key) use (&$retval) { 31 | $arr = []; 32 | $group->each(function($item) use (&$arr) { 33 | $arr[$item['key']] = $item['parsed']; 34 | }); 35 | $retval[$key] = $arr; 36 | }); 37 | return $retval; 38 | } 39 | 40 | static function defaults() 41 | { 42 | return [ 43 | 'general' => [ 44 | 'copyright' => 'Sabatino Masala' 45 | ], 46 | 'seo' => [ 47 | 'title' => 'Filament marketing starter', 48 | ], 49 | 'routes' => [ 50 | 'blog' => '/blog', 51 | ], 52 | 'nav' => [ 53 | 'nav_cta_text' => 'Fork me on Github', 54 | 'nav_cta_link' => 'https://github.com/sabatinomasala/filament-marketing-starter', 55 | 'banner' => 'Welcome to the starter kit!', 56 | 'navlinks' => [ 57 | 'type' => 'navlink', 58 | 'data' => [ 59 | [ 60 | 'name' => 'Blog', 61 | 'link' => '/en/blog' 62 | ] 63 | ] 64 | ], 65 | ], 66 | 'footer' => [ 67 | 'linkgroups' => [ 68 | 'type' => 'footer', 69 | 'data' => [ 70 | [ 71 | 'title' => 'Socials', 72 | 'links' => [ 73 | [ 74 | 'name' => 'Youtube Channel', 75 | 'link' => 'https://www.youtube.com/channel/UCU1VNvcwKDmsHqbTtUOaZ-w' 76 | ], 77 | [ 78 | 'name' => 'Github', 79 | 'link' => 'https://github.com/sabatinoMasala' 80 | ] 81 | ] 82 | ] 83 | ] 84 | ], 85 | ] 86 | ]; 87 | } 88 | public function parsed() 89 | { 90 | if (!empty($this->value)) { 91 | try { 92 | return array_values($this->value)[0]['data']['value']; 93 | } catch (\Exception $e) { 94 | \Log::error('Could not parse setting ' . $this->key); 95 | } 96 | } 97 | return null; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /config/responsecache.php: -------------------------------------------------------------------------------- 1 | env('RESPONSE_CACHE_ENABLED', true), 10 | 11 | /* 12 | * The given class will determinate if a request should be cached. The 13 | * default class will cache all successful GET-requests. 14 | * 15 | * You can provide your own class given that it implements the 16 | * CacheProfile interface. 17 | */ 18 | 'cache_profile' => InertiaAwareCacheProfile::class, 19 | 20 | /* 21 | * Optionally, you can specify a header that will force a cache bypass. 22 | * This can be useful to monitor the performance of your application. 23 | */ 24 | 'cache_bypass_header' => [ 25 | 'name' => env('CACHE_BYPASS_HEADER_NAME', null), 26 | 'value' => env('CACHE_BYPASS_HEADER_VALUE', null), 27 | ], 28 | 29 | /* 30 | * When using the default CacheRequestFilter this setting controls the 31 | * default number of seconds responses must be cached. 32 | */ 33 | 'cache_lifetime_in_seconds' => (int) env('RESPONSE_CACHE_LIFETIME', 60 * 60 * 24 * 7), 34 | 35 | /* 36 | * This setting determines if a http header named with the cache time 37 | * should be added to a cached response. This can be handy when 38 | * debugging. 39 | */ 40 | 'add_cache_time_header' => env('APP_DEBUG', false), 41 | 42 | /* 43 | * This setting determines the name of the http header that contains 44 | * the time at which the response was cached 45 | */ 46 | 'cache_time_header_name' => env('RESPONSE_CACHE_HEADER_NAME', 'laravel-responsecache'), 47 | 48 | /* 49 | * This setting determines if a http header named with the cache age 50 | * should be added to a cached response. This can be handy when 51 | * debugging. 52 | * ONLY works when "add_cache_time_header" is also active! 53 | */ 54 | 'add_cache_age_header' => env('RESPONSE_CACHE_AGE_HEADER', false), 55 | 56 | /* 57 | * This setting determines the name of the http header that contains 58 | * the age of cache 59 | */ 60 | 'cache_age_header_name' => env('RESPONSE_CACHE_AGE_HEADER_NAME', 'laravel-responsecache-age'), 61 | 62 | /* 63 | * Here you may define the cache store that should be used to store 64 | * requests. This can be the name of any store that is 65 | * configured in app/config/cache.php 66 | */ 67 | 'cache_store' => env('RESPONSE_CACHE_DRIVER', 'file'), 68 | 69 | /* 70 | * Here you may define replacers that dynamically replace content from the response. 71 | * Each replacer must implement the Replacer interface. 72 | */ 73 | 'replacers' => [ 74 | \Spatie\ResponseCache\Replacers\CsrfTokenReplacer::class, 75 | ], 76 | 77 | /* 78 | * If the cache driver you configured supports tags, you may specify a tag name 79 | * here. All responses will be tagged. When clearing the responsecache only 80 | * items with that tag will be flushed. 81 | * 82 | * You may use a string or an array here. 83 | */ 84 | 'cache_tag' => '', 85 | 86 | /* 87 | * This class is responsible for generating a hash for a request. This hash 88 | * is used to look up a cached response. 89 | */ 90 | 'hasher' => \Spatie\ResponseCache\Hasher\DefaultHasher::class, 91 | 92 | /* 93 | * This class is responsible for serializing responses. 94 | */ 95 | 'serializer' => \Spatie\ResponseCache\Serializers\DefaultSerializer::class, 96 | ]; 97 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORE', 'database'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "array", "database", "file", "memcached", 30 | | "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'array' => [ 37 | 'driver' => 'array', 38 | 'serialize' => false, 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'connection' => env('DB_CACHE_CONNECTION'), 44 | 'table' => env('DB_CACHE_TABLE', 'cache'), 45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 46 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'), 47 | ], 48 | 49 | 'file' => [ 50 | 'driver' => 'file', 51 | 'path' => storage_path('framework/cache/data'), 52 | 'lock_path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 77 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 78 | ], 79 | 80 | 'dynamodb' => [ 81 | 'driver' => 'dynamodb', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 86 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 87 | ], 88 | 89 | 'octane' => [ 90 | 'driver' => 'octane', 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Cache Key Prefix 98 | |-------------------------------------------------------------------------- 99 | | 100 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 101 | | stores, there might be other applications using the same cache. For 102 | | that reason, you may prefix every cache key to avoid collisions. 103 | | 104 | */ 105 | 106 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 107 | 108 | ]; 109 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'log'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Mailer Configurations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Here you may configure all of the mailers used by your application plus 25 | | their respective settings. Several examples have been configured for 26 | | you and you are free to add your own as your application requires. 27 | | 28 | | Laravel supports a variety of mail "transport" drivers that can be used 29 | | when delivering an email. You may specify which one you're using for 30 | | your mailers below. You may also add additional mailers if needed. 31 | | 32 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 33 | | "postmark", "resend", "log", "array", 34 | | "failover", "roundrobin" 35 | | 36 | */ 37 | 38 | 'mailers' => [ 39 | 40 | 'smtp' => [ 41 | 'transport' => 'smtp', 42 | 'url' => env('MAIL_URL'), 43 | 'host' => env('MAIL_HOST', '127.0.0.1'), 44 | 'port' => env('MAIL_PORT', 2525), 45 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 46 | 'username' => env('MAIL_USERNAME'), 47 | 'password' => env('MAIL_PASSWORD'), 48 | 'timeout' => null, 49 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 50 | ], 51 | 52 | 'ses' => [ 53 | 'transport' => 'ses', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 59 | // 'client' => [ 60 | // 'timeout' => 5, 61 | // ], 62 | ], 63 | 64 | 'resend' => [ 65 | 'transport' => 'resend', 66 | ], 67 | 68 | 'sendmail' => [ 69 | 'transport' => 'sendmail', 70 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 71 | ], 72 | 73 | 'log' => [ 74 | 'transport' => 'log', 75 | 'channel' => env('MAIL_LOG_CHANNEL'), 76 | ], 77 | 78 | 'array' => [ 79 | 'transport' => 'array', 80 | ], 81 | 82 | 'failover' => [ 83 | 'transport' => 'failover', 84 | 'mailers' => [ 85 | 'smtp', 86 | 'log', 87 | ], 88 | ], 89 | 90 | 'roundrobin' => [ 91 | 'transport' => 'roundrobin', 92 | 'mailers' => [ 93 | 'ses', 94 | 'postmark', 95 | ], 96 | ], 97 | 98 | ], 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Global "From" Address 103 | |-------------------------------------------------------------------------- 104 | | 105 | | You may wish for all emails sent by your application to be sent from 106 | | the same address. Here you may specify a name and address that is 107 | | used globally for all emails that are sent by your application. 108 | | 109 | */ 110 | 111 | 'from' => [ 112 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 113 | 'name' => env('MAIL_FROM_NAME', 'Example'), 114 | ], 115 | 116 | ]; 117 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'database'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection options for every queue backend 24 | | used by your application. An example configuration is provided for 25 | | each backend supported by Laravel. You're also free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'connection' => env('DB_QUEUE_CONNECTION'), 40 | 'table' => env('DB_QUEUE_TABLE', 'jobs'), 41 | 'queue' => env('DB_QUEUE', 'default'), 42 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 43 | 'after_commit' => false, 44 | ], 45 | 46 | 'beanstalkd' => [ 47 | 'driver' => 'beanstalkd', 48 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), 49 | 'queue' => env('BEANSTALKD_QUEUE', 'default'), 50 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 51 | 'block_for' => 0, 52 | 'after_commit' => false, 53 | ], 54 | 55 | 'sqs' => [ 56 | 'driver' => 'sqs', 57 | 'key' => env('AWS_ACCESS_KEY_ID'), 58 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 59 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 60 | 'queue' => env('SQS_QUEUE', 'default'), 61 | 'suffix' => env('SQS_SUFFIX'), 62 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 63 | 'after_commit' => false, 64 | ], 65 | 66 | 'redis' => [ 67 | 'driver' => 'redis', 68 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 69 | 'queue' => env('REDIS_QUEUE', 'default'), 70 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 71 | 'block_for' => null, 72 | 'after_commit' => false, 73 | ], 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Job Batching 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The following options configure the database and table that store job 83 | | batching information. These options can be updated to any database 84 | | connection and table which has been defined by your application. 85 | | 86 | */ 87 | 88 | 'batching' => [ 89 | 'database' => env('DB_CONNECTION', 'sqlite'), 90 | 'table' => 'job_batches', 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Failed Queue Jobs 96 | |-------------------------------------------------------------------------- 97 | | 98 | | These options configure the behavior of failed queue job logging so you 99 | | can control how and where failed jobs are stored. Laravel ships with 100 | | support for storing failed jobs in a simple file or in a database. 101 | | 102 | | Supported drivers: "database-uuids", "dynamodb", "file", "null" 103 | | 104 | */ 105 | 106 | 'failed' => [ 107 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 108 | 'database' => env('DB_CONNECTION', 'sqlite'), 109 | 'table' => 'failed_jobs', 110 | ], 111 | 112 | ]; 113 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => env('AUTH_GUARD', 'web'), 18 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | which utilizes session storage plus the Eloquent user provider. 29 | | 30 | | All authentication guards have a user provider, which defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | system used by the application. Typically, Eloquent is utilized. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication guards have a user provider, which defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | system used by the application. Typically, Eloquent is utilized. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | providers to represent the model / table. These providers may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'eloquent', 65 | 'model' => env('AUTH_MODEL', App\Models\User::class), 66 | ], 67 | 68 | // 'users' => [ 69 | // 'driver' => 'database', 70 | // 'table' => 'users', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Resetting Passwords 77 | |-------------------------------------------------------------------------- 78 | | 79 | | These configuration options specify the behavior of Laravel's password 80 | | reset functionality, including the table utilized for token storage 81 | | and the user provider that is invoked to actually retrieve users. 82 | | 83 | | The expiry time is the number of minutes that each reset token will be 84 | | considered valid. This security feature keeps tokens short-lived so 85 | | they have less time to be guessed. You may change this as needed. 86 | | 87 | | The throttle setting is the number of seconds a user must wait before 88 | | generating more password reset tokens. This prevents the user from 89 | | quickly generating a very large amount of password reset tokens. 90 | | 91 | */ 92 | 93 | 'passwords' => [ 94 | 'users' => [ 95 | 'provider' => 'users', 96 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 97 | 'expire' => 60, 98 | 'throttle' => 60, 99 | ], 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Password Confirmation Timeout 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Here you may define the amount of seconds before a password confirmation 108 | | window expires and users are asked to re-enter their password via the 109 | | confirmation screen. By default, the timeout lasts for three hours. 110 | | 111 | */ 112 | 113 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 114 | 115 | ]; 116 | -------------------------------------------------------------------------------- /app/Filament/Resources/SettingResource.php: -------------------------------------------------------------------------------- 1 | schema([ 32 | TextInput::make('key')->required(), 33 | TextInput::make('group')->required(), 34 | Builder::make('value') 35 | ->blocks([ 36 | Builder\Block::make('string') 37 | ->schema([ 38 | TextInput::make('value'), 39 | ]), 40 | Builder\Block::make('rich_editor') 41 | ->schema([ 42 | RichEditor::make('value'), 43 | ]), 44 | Builder\Block::make('navlink') 45 | ->schema([ 46 | Repeater::make('value')->schema([ 47 | TextInput::make('name'), 48 | Textarea::make('link'), 49 | ]) 50 | ->collapsed() 51 | ->collapsible() 52 | ->cloneable(), 53 | ]), 54 | Builder\Block::make('footer') 55 | ->schema([ 56 | Repeater::make('value')->schema([ 57 | TextInput::make('title'), 58 | Repeater::make('links')->schema([ 59 | TextInput::make('name'), 60 | Textarea::make('link'), 61 | ]) 62 | ->collapsed() 63 | ->collapsible() 64 | ->cloneable(), 65 | ]) 66 | ->collapsed() 67 | ->collapsible() 68 | ->cloneable(), 69 | ]), 70 | ]) 71 | ->collapsible() 72 | ->maxItems(1) 73 | ->reorderable(false) 74 | ->columnSpanFull() 75 | ]); 76 | } 77 | 78 | public static function table(Table $table): Table 79 | { 80 | return $table 81 | ->columns([ 82 | TextColumn::make('key')->searchable(), 83 | TextColumn::make('group') 84 | ->searchable() 85 | ->sortable(), 86 | ]) 87 | ->filters([ 88 | // 89 | ]) 90 | ->actions([ 91 | Tables\Actions\EditAction::make(), 92 | ]) 93 | ->bulkActions([ 94 | Tables\Actions\BulkActionGroup::make([ 95 | Tables\Actions\DeleteBulkAction::make(), 96 | ]), 97 | ]); 98 | } 99 | 100 | public static function getRelations(): array 101 | { 102 | return [ 103 | // 104 | ]; 105 | } 106 | 107 | public static function getPages(): array 108 | { 109 | return [ 110 | 'index' => Pages\ListSettings::route('/'), 111 | 'create' => Pages\CreateSetting::route('/create'), 112 | 'edit' => Pages\EditSetting::route('/{record}/edit'), 113 | ]; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const animate = require("tailwindcss-animate") 2 | import forms from '@tailwindcss/forms'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | module.exports = { 6 | darkMode: ["class"], 7 | safelist: [ 8 | { 9 | pattern: /bg-(red)-(100|200|300|400|600|700|800|900|950)/, 10 | }, 11 | 'bg-black', 12 | 'dark' 13 | ], 14 | prefix: "", 15 | 16 | content: [ 17 | "./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php", 18 | "./storage/framework/views/*.php", 19 | "./resources/views/**/*.blade.php", 20 | "./resources/js/**/*.{js,jsx,vue}", 21 | ], 22 | 23 | theme: { 24 | container: { 25 | center: true, 26 | padding: "2rem", 27 | screens: { 28 | "2xl": "1400px", 29 | }, 30 | }, 31 | extend: { 32 | colors: { 33 | transparent: 'transparent', 34 | current: 'currentColor', 35 | white: '#ffffff', 36 | black: '#000000', 37 | cherry: '#800f4d', 38 | red: { 39 | '50': '#fef2f3', 40 | '100': '#fde3e6', 41 | '200': '#fdcbd0', 42 | '300': '#faa7af', 43 | '400': '#f47582', 44 | '500': '#e83345', 45 | '600': '#d72b3c', 46 | '700': '#b5202f', 47 | '800': '#961e2a', 48 | '900': '#7c2029', 49 | '950': '#430c11', 50 | }, 51 | border: "var(--border)", 52 | input: "var(--input)", 53 | ring: "var(--ring)", 54 | background: "var(--background)", 55 | foreground: "var(--foreground)", 56 | primary: { 57 | DEFAULT: "var(--primary)", 58 | foreground: "var(--primary-foreground)", 59 | }, 60 | secondary: { 61 | DEFAULT: "var(--secondary)", 62 | foreground: "var(--secondary-foreground)", 63 | }, 64 | destructive: { 65 | DEFAULT: "var(--destructive)", 66 | foreground: "var(--destructive-foreground)", 67 | }, 68 | muted: { 69 | DEFAULT: "var(--muted)", 70 | foreground: "var(--muted-foreground)", 71 | }, 72 | accent: { 73 | DEFAULT: "var(--accent)", 74 | foreground: "var(--accent-foreground)", 75 | }, 76 | popover: { 77 | DEFAULT: "var(--popover)", 78 | foreground: "var(--popover-foreground)", 79 | }, 80 | card: { 81 | DEFAULT: "var(--card)", 82 | foreground: "var(--card-foreground)", 83 | }, 84 | }, 85 | borderRadius: { 86 | xl: "calc(var(--radius) + 4px)", 87 | lg: "var(--radius)", 88 | md: "calc(var(--radius) - 2px)", 89 | sm: "calc(var(--radius) - 4px)", 90 | }, 91 | keyframes: { 92 | "accordion-down": { 93 | from: { height: 0 }, 94 | to: { height: "var(--radix-accordion-content-height)" }, 95 | }, 96 | "accordion-up": { 97 | from: { height: "var(--radix-accordion-content-height)" }, 98 | to: { height: 0 }, 99 | }, 100 | "collapsible-down": { 101 | from: { height: 0 }, 102 | to: { height: 'var(--radix-collapsible-content-height)' }, 103 | }, 104 | "collapsible-up": { 105 | from: { height: 'var(--radix-collapsible-content-height)' }, 106 | to: { height: 0 }, 107 | }, 108 | }, 109 | animation: { 110 | "accordion-down": "accordion-down 0.2s ease-out", 111 | "accordion-up": "accordion-up 0.2s ease-out", 112 | "collapsible-down": "collapsible-down 0.2s ease-in-out", 113 | "collapsible-up": "collapsible-up 0.2s ease-in-out", 114 | }, 115 | }, 116 | }, 117 | plugins: [animate, forms], 118 | } 119 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Environment 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "environment" your application is currently 24 | | running in. This may determine how you prefer to configure various 25 | | services the application utilizes. Set this in your ".env" file. 26 | | 27 | */ 28 | 29 | 'env' => env('APP_ENV', 'production'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Debug Mode 34 | |-------------------------------------------------------------------------- 35 | | 36 | | When your application is in debug mode, detailed error messages with 37 | | stack traces will be shown on every error that occurs within your 38 | | application. If disabled, a simple generic error page is shown. 39 | | 40 | */ 41 | 42 | 'debug' => (bool) env('APP_DEBUG', false), 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Application URL 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This URL is used by the console to properly generate URLs when using 50 | | the Artisan command line tool. You should set this to the root of 51 | | the application so that it's available within Artisan commands. 52 | | 53 | */ 54 | 55 | 'url' => env('APP_URL', 'http://localhost'), 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Application Timezone 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Here you may specify the default timezone for your application, which 63 | | will be used by the PHP date and date-time functions. The timezone 64 | | is set to "UTC" by default as it is suitable for most use cases. 65 | | 66 | */ 67 | 68 | 'timezone' => env('APP_TIMEZONE', 'UTC'), 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Application Locale Configuration 73 | |-------------------------------------------------------------------------- 74 | | 75 | | The application locale determines the default locale that will be used 76 | | by Laravel's translation / localization methods. This option can be 77 | | set to any locale for which you plan to have translation strings. 78 | | 79 | */ 80 | 81 | 'locale' => env('APP_LOCALE', 'en'), 82 | 83 | 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), 84 | 85 | 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Encryption Key 90 | |-------------------------------------------------------------------------- 91 | | 92 | | This key is utilized by Laravel's encryption services and should be set 93 | | to a random, 32 character string to ensure that all encrypted values 94 | | are secure. You should do this prior to deploying the application. 95 | | 96 | */ 97 | 98 | 'cipher' => 'AES-256-CBC', 99 | 100 | 'key' => env('APP_KEY'), 101 | 102 | 'previous_keys' => [ 103 | ...array_filter( 104 | explode(',', env('APP_PREVIOUS_KEYS', '')) 105 | ), 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Maintenance Mode Driver 111 | |-------------------------------------------------------------------------- 112 | | 113 | | These configuration options determine the driver used to determine and 114 | | manage Laravel's "maintenance mode" status. The "cache" driver will 115 | | allow maintenance mode to be controlled across multiple machines. 116 | | 117 | | Supported drivers: "file", "cache" 118 | | 119 | */ 120 | 121 | 'maintenance' => [ 122 | 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 123 | 'store' => env('APP_MAINTENANCE_STORE', 'database'), 124 | ], 125 | 126 | ]; 127 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Deprecations Log Channel 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This option controls the log channel that should be used to log warnings 29 | | regarding deprecated PHP and library features. This allows you to get 30 | | your application ready for upcoming major versions of dependencies. 31 | | 32 | */ 33 | 34 | 'deprecations' => [ 35 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 36 | 'trace' => env('LOG_DEPRECATIONS_TRACE', false), 37 | ], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Log Channels 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Here you may configure the log channels for your application. Laravel 45 | | utilizes the Monolog PHP logging library, which includes a variety 46 | | of powerful log handlers and formatters that you're free to use. 47 | | 48 | | Available drivers: "single", "daily", "slack", "syslog", 49 | | "errorlog", "monolog", "custom", "stack" 50 | | 51 | */ 52 | 53 | 'channels' => [ 54 | 55 | 'stack' => [ 56 | 'driver' => 'stack', 57 | 'channels' => explode(',', env('LOG_STACK', 'single')), 58 | 'ignore_exceptions' => false, 59 | ], 60 | 61 | 'single' => [ 62 | 'driver' => 'single', 63 | 'path' => storage_path('logs/laravel.log'), 64 | 'level' => env('LOG_LEVEL', 'debug'), 65 | 'replace_placeholders' => true, 66 | ], 67 | 68 | 'daily' => [ 69 | 'driver' => 'daily', 70 | 'path' => storage_path('logs/laravel.log'), 71 | 'level' => env('LOG_LEVEL', 'debug'), 72 | 'days' => env('LOG_DAILY_DAYS', 14), 73 | 'replace_placeholders' => true, 74 | ], 75 | 76 | 'slack' => [ 77 | 'driver' => 'slack', 78 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 79 | 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), 80 | 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 81 | 'level' => env('LOG_LEVEL', 'critical'), 82 | 'replace_placeholders' => true, 83 | ], 84 | 85 | 'papertrail' => [ 86 | 'driver' => 'monolog', 87 | 'level' => env('LOG_LEVEL', 'debug'), 88 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 89 | 'handler_with' => [ 90 | 'host' => env('PAPERTRAIL_URL'), 91 | 'port' => env('PAPERTRAIL_PORT'), 92 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), 93 | ], 94 | 'processors' => [PsrLogMessageProcessor::class], 95 | ], 96 | 97 | 'stderr' => [ 98 | 'driver' => 'monolog', 99 | 'level' => env('LOG_LEVEL', 'debug'), 100 | 'handler' => StreamHandler::class, 101 | 'formatter' => env('LOG_STDERR_FORMATTER'), 102 | 'with' => [ 103 | 'stream' => 'php://stderr', 104 | ], 105 | 'processors' => [PsrLogMessageProcessor::class], 106 | ], 107 | 108 | 'syslog' => [ 109 | 'driver' => 'syslog', 110 | 'level' => env('LOG_LEVEL', 'debug'), 111 | 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 112 | 'replace_placeholders' => true, 113 | ], 114 | 115 | 'errorlog' => [ 116 | 'driver' => 'errorlog', 117 | 'level' => env('LOG_LEVEL', 'debug'), 118 | 'replace_placeholders' => true, 119 | ], 120 | 121 | 'null' => [ 122 | 'driver' => 'monolog', 123 | 'handler' => NullHandler::class, 124 | ], 125 | 126 | 'emergency' => [ 127 | 'path' => storage_path('logs/laravel.log'), 128 | ], 129 | 130 | ], 131 | 132 | ]; 133 | -------------------------------------------------------------------------------- /resources/views/vendor/livewire/simple-tailwind.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | if (! isset($scrollTo)) { 3 | $scrollTo = 'body'; 4 | } 5 | 6 | $scrollIntoViewJsSnippet = ($scrollTo !== false) 7 | ? << 14 | @if ($paginator->hasPages()) 15 | 55 | @endif 56 | 57 | --------------------------------------------------------------------------------