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 |
2 |
3 |
4 |
5 |
6 | {{ props.data.title }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
{{ stat.label }}
14 | {{ stat.quantity }}
15 |
16 |
17 |
18 |
19 |
20 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | {{ data.subtitle }}
8 |
9 |
10 | {{ data.title }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ feature.title }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
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 |
2 |
3 |
4 |
5 |
6 | {{ data.title }}
7 |
8 |
9 |
10 |
11 |
12 | {{ faq.question }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
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 |
28 |
29 |
30 |
31 |
{{ props.status }}
32 |
{{ title }}
33 |
{{ description }}
34 |
35 | Go back home
36 | Contact support →
37 |
38 |
39 |
40 |
41 |
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 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 | {{ button.label }} →
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
2 |
3 |
4 | Previous
5 |
6 |
7 |
8 | {{ i }}
9 |
10 |
11 | Next
12 |
13 |
14 |
15 |
16 |
41 |
--------------------------------------------------------------------------------
/resources/js/Components/blocks/TitleSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ data.subtitle }}
7 |
8 |
9 | {{ data.title }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 | {{ button.label }}
25 |
26 |
27 |
28 |
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 |
40 |
41 |
44 | {
53 | const originalEvent = event.detail.originalEvent;
54 | const target = originalEvent.target;
55 | if (
56 | originalEvent.offsetX > target.clientWidth ||
57 | originalEvent.offsetY > target.clientHeight
58 | ) {
59 | event.preventDefault();
60 | }
61 | }
62 | "
63 | >
64 |
65 |
66 |
69 |
70 | Close
71 |
72 |
73 |
74 |
75 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
{{ page.name }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
TAGS TODO
21 |
22 | {{ post.title }}
23 |
24 |
25 | {{ post.short_description }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
51 |
--------------------------------------------------------------------------------
/resources/js/Components/ui/dialog/DialogContent.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
44 |
53 |
54 |
55 |
58 |
59 | Close
60 |
61 |
62 |
63 |
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 |
16 |
51 |
52 | @endif
53 |
54 |
--------------------------------------------------------------------------------
/resources/js/Components/blocks/DetailSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ data.supertitle }}
9 |
10 |
11 | {{ data.title }}
12 |
13 |
14 |
15 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
{{ post.datetime_readable }}
13 |
{{ post.category.title }}
14 |
15 |
16 |
17 |
18 |
19 | {{ post.title }}
20 |
21 |
22 |
{{ post.short_description }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ post.author.name }}
31 |
32 |
33 |
{{ post.author.role }}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
43 |
44 |
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 |
16 |
17 | {{-- Previous Page Link --}}
18 | @if ($paginator->onFirstPage())
19 |
20 | {!! __('pagination.previous') !!}
21 |
22 | @else
23 | @if(method_exists($paginator,'getCursorName'))
24 |
25 | {!! __('pagination.previous') !!}
26 |
27 | @else
28 |
30 | {!! __('pagination.previous') !!}
31 |
32 | @endif
33 | @endif
34 |
35 |
36 |
37 | {{-- Next Page Link --}}
38 | @if ($paginator->hasMorePages())
39 | @if(method_exists($paginator,'getCursorName'))
40 |
41 | {!! __('pagination.next') !!}
42 |
43 | @else
44 |
45 | {!! __('pagination.next') !!}
46 |
47 | @endif
48 | @else
49 |
50 | {!! __('pagination.next') !!}
51 |
52 | @endif
53 |
54 |
55 | @endif
56 |
57 |
--------------------------------------------------------------------------------