diff --git a/app/Filament/Resources/Visitors/Pages/ManageVisitors.php b/app/Filament/Resources/Visitors/Pages/ManageVisitors.php new file mode 100644 index 0000000..38ea988 --- /dev/null +++ b/app/Filament/Resources/Visitors/Pages/ManageVisitors.php @@ -0,0 +1,13 @@ +columns([ + TextColumn::make('date') + ->label('Tanggal & Waktu') + ->date('l, d F Y') + ->sortable(), + + TextColumn::make('route') + ->label('Halaman') + ->searchable() + ->sortable(), + + TextColumn::make('method') + ->label('Method') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'GET' => 'success', + 'POST' => 'warning', + default => 'gray', + }), + + TextColumn::make('status') + ->label('Status') + ->badge() + ->color(fn (int $state): string => $state >= 200 && $state < 300 ? 'success' : 'danger'), + + TextColumn::make('ip') + ->label('IP Address') + ->searchable(), + + TextColumn::make('counter') + ->label('Hits') + ->numeric() + ->sortable(), + + TextColumn::make('user.name') + ->label('User') + ->placeholder('Guest'), + ]) + ->defaultSort('date', 'desc') + ->filters([ + SelectFilter::make('method') + ->options([ + 'GET' => 'GET', + 'POST' => 'POST', + ]), + + Filter::make('date') + ->schema([ + DatePicker::make('from')->time(), + DatePicker::make('until')->time(), + ]) + ->query(function ($query, array $data) { + return $query + ->when($data['from'], fn ($q) => $q->whereDate('date', '>=', $data['from'])) + ->when($data['until'], fn ($q) => $q->whereDate('date', '<=', $data['until'])); + }), + ]) + ->recordActions([]) + ->toolbarActions([]); + } + + public static function getPages(): array + { + return [ + 'index' => ManageVisitors::route('/'), + ]; + } +} diff --git a/app/Http/Middleware/TrackVisitor.php b/app/Http/Middleware/TrackVisitor.php new file mode 100644 index 0000000..dfabfe3 --- /dev/null +++ b/app/Http/Middleware/TrackVisitor.php @@ -0,0 +1,55 @@ +method() === 'GET' && $response->getStatusCode() === 200 && ! $request->header('X-Livewire')) { + $ip = $request->ip(); + $route = $request->route() ? $request->route()->getName() : $request->path(); + + // Skip if route is not defined or route is Livewire + if (! $route || str_contains($route, 'livewire')) { + return $response; + } + + $today = Carbon::today(); + + // Check if IP has already visited this route today + $alreadyVisited = Visitor::where('ip', $ip) + ->where('route', $route) + ->whereDate('date', $today) + ->exists(); + + if (! $alreadyVisited) { + Visitor::create([ + 'user_id' => auth()->id(), + 'method' => $request->method(), + 'route' => $route, + 'status' => $response->getStatusCode(), + 'ip' => $ip, + 'date' => Carbon::now(), + 'counter' => 1, // Set 1 because this is the first unique visit today + ]); + } + } + + return $response; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 8a3cf69..9117a81 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -76,4 +76,9 @@ public function verificationReviews(): HasMany { return $this->hasMany(VerificationReview::class, 'reviewer_id'); } + + public function visitors(): HasMany + { + return $this->hasMany(Visitor::class); + } } diff --git a/app/Models/Visitor.php b/app/Models/Visitor.php new file mode 100644 index 0000000..384c8e7 --- /dev/null +++ b/app/Models/Visitor.php @@ -0,0 +1,16 @@ +belongsTo(User::class); + } +} diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index ee67c65..0b3eb90 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -2,9 +2,11 @@ namespace App\Providers; +use App\Models\Visitor; use App\Settings\GeneralSettings; use App\Settings\SeoSettings; use App\Settings\SocialMediaSettings; +use Carbon\Carbon; use Illuminate\Support\ServiceProvider; class ViewServiceProvider extends ServiceProvider @@ -25,5 +27,23 @@ public function boot(): void view()->share('general', app(GeneralSettings::class)); view()->share('social', app(SocialMediaSettings::class)); view()->share('seo', app(SeoSettings::class)); + + view()->composer('components.partials.footer', function ($view) { + $routeStatisticModel = config('route-statistics.model', Visitor::class); + + $today = $routeStatisticModel::whereDate('date', Carbon::today())->sum('counter'); + $yesterday = $routeStatisticModel::whereDate('date', Carbon::yesterday())->sum('counter'); + $thisMonth = $routeStatisticModel::whereMonth('date', Carbon::now()->month) + ->whereYear('date', Carbon::now()->year) + ->sum('counter'); + $total = $routeStatisticModel::sum('counter'); + + $view->with('visitorStats', [ + 'today' => number_format($today), + 'yesterday' => number_format($yesterday), + 'thisMonth' => number_format($thisMonth), + 'total' => number_format($total), + ]); + }); } } diff --git a/composer.json b/composer.json index 207d4d6..f217c1c 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "php": "^8.2", "asmit/filament-upload": "^1.1", "bezhansalleh/filament-shield": "^4.0", + "bilfeldt/laravel-route-statistics": "^4.2", "filament/filament": "^4.0", "filament/spatie-laravel-media-library-plugin": "^4.0", "filament/spatie-laravel-settings-plugin": "^4.4", diff --git a/composer.lock b/composer.lock index 872a357..1779b0c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "37c52f60c416d783d4922313800eb7b5", + "content-hash": "a8225cac91593cfe6465c7be71fbbdc6", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -292,6 +292,214 @@ ], "time": "2025-12-26T09:31:27+00:00" }, + { + "name": "bilfeldt/laravel-correlation-id", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/bilfeldt/laravel-correlation-id.git", + "reference": "0a12cddc777d84529b798f5c1105bbd227c7e2e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bilfeldt/laravel-correlation-id/zipball/0a12cddc777d84529b798f5c1105bbd227c7e2e8", + "reference": "0a12cddc777d84529b798f5c1105bbd227c7e2e8", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0 || ^11.0 || ^12.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "nunomaduro/collision": "^7.8 || ^8.0", + "orchestra/testbench": "^8.0 || ^9.0 || ^10.0", + "phpunit/phpunit": "^10.0 || ^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Bilfeldt\\CorrelationId\\CorrelationIdServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Bilfeldt\\CorrelationId\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anders Bilfeldt", + "email": "abilfeldt@gmail.com", + "role": "Developer" + } + ], + "description": "Deal with Request-ID and Correlation-ID in Laravel applications", + "homepage": "https://github.com/bilfeldt/laravel-correlation-id", + "keywords": [ + "bilfeldt", + "correlation id", + "request id" + ], + "support": { + "issues": "https://github.com/bilfeldt/laravel-correlation-id/issues", + "source": "https://github.com/bilfeldt/laravel-correlation-id" + }, + "funding": [ + { + "url": "https://github.com/bilfeldt", + "type": "github" + } + ], + "time": "2025-11-28T18:40:34+00:00" + }, + { + "name": "bilfeldt/laravel-request-logger", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/bilfeldt/laravel-request-logger.git", + "reference": "40c22bbddc469a38dee1a4574bcd809ada352813" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bilfeldt/laravel-request-logger/zipball/40c22bbddc469a38dee1a4574bcd809ada352813", + "reference": "40c22bbddc469a38dee1a4574bcd809ada352813", + "shasum": "" + }, + "require": { + "bilfeldt/laravel-correlation-id": "^1.0", + "ext-json": "*", + "illuminate/contracts": "^10.0 || ^11.0 || ^12.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "nunomaduro/collision": "^7.2 || ^8.0", + "orchestra/testbench": "^8.0 || ^9.0 || ^10.0", + "phpunit/phpunit": "^10.0 || ^11.5.3", + "spatie/laravel-ray": "^1.32" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "RequestLogger": "Bilfeldt\\RequestLogger\\RequestLoggerFacade" + }, + "providers": [ + "Bilfeldt\\RequestLogger\\RequestLoggerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Bilfeldt\\RequestLogger\\": "src", + "Bilfeldt\\RequestLogger\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anders Bilfeldt", + "email": "abilfeldt@gmail.com", + "role": "Developer" + } + ], + "description": "Log Laravel application request and responses for debugging or statistics", + "homepage": "https://github.com/bilfeldt/laravel-request-logger", + "keywords": [ + "bilfeldt", + "laravel", + "laravel-request-logger" + ], + "support": { + "issues": "https://github.com/bilfeldt/laravel-route-statistics/issues", + "source": "https://github.com/bilfeldt/laravel-route-statistics" + }, + "funding": [ + { + "url": "https://github.com/bilfeldt", + "type": "github" + } + ], + "time": "2025-02-17T09:51:14+00:00" + }, + { + "name": "bilfeldt/laravel-route-statistics", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/bilfeldt/laravel-route-statistics.git", + "reference": "9b314244248f43615049f102ba563ed5b7858db0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bilfeldt/laravel-route-statistics/zipball/9b314244248f43615049f102ba563ed5b7858db0", + "reference": "9b314244248f43615049f102ba563ed5b7858db0", + "shasum": "" + }, + "require": { + "bilfeldt/laravel-request-logger": "^3.0", + "illuminate/contracts": "^10.0 || ^11.0 || ^12.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "nunomaduro/collision": "^7.2 || ^8.0", + "orchestra/testbench": "^8.0 || ^9.0 || ^10.0", + "phpunit/phpunit": "^10.0 || ^11.5.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Bilfeldt\\LaravelRouteStatistics\\LaravelRouteStatisticsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Bilfeldt\\LaravelRouteStatistics\\": "src", + "Bilfeldt\\LaravelRouteStatistics\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anders Bilfeldt", + "email": "abilfeldt@gmail.com", + "role": "Developer" + } + ], + "description": "Log statistics about route usage per user/team", + "homepage": "https://github.com/bilfeldt/laravel-route-statistics", + "keywords": [ + "bilfeldt", + "laravel", + "route", + "statistics" + ], + "support": { + "issues": "https://github.com/bilfeldt/laravel-route-statistics/issues", + "source": "https://github.com/bilfeldt/laravel-route-statistics" + }, + "funding": [ + { + "url": "https://github.com/bilfeldt", + "type": "github" + } + ], + "time": "2025-04-12T20:18:06+00:00" + }, { "name": "blade-ui-kit/blade-heroicons", "version": "2.6.0", diff --git a/config/route-statistics.php b/config/route-statistics.php new file mode 100644 index 0000000..2d8a352 --- /dev/null +++ b/config/route-statistics.php @@ -0,0 +1,74 @@ + env('ROUTE_STATISTICS_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Store parameters + |-------------------------------------------------------------------------- + | + | If this setting is set to true the route parameters will also be logged. + | + */ + 'store_route_parameters' => env('ROUTE_STATISTICS_STORE_ROUTE_PARAMETERS', false), + + /* + |-------------------------------------------------------------------------- + | Aggregation + |-------------------------------------------------------------------------- + | + | This setting controls how we should aggregate requests. + | Possible values are: SECOND, MINUTE, HOUR, DAY, MONTH, YEAR + | + */ + 'aggregate' => env('ROUTE_STATISTICS_AGGREGATE', 'DAY'), + + /* + |-------------------------------------------------------------------------- + | Model + |-------------------------------------------------------------------------- + | + | This is the model used to store request statistics. + | It is possible to implement a custom model which extends the default model + | or alternatively implement a completely new model which implements + | Bilfeldt\RequestLogger\Contracts\RequestLoggerInterface + | + */ + 'model' => env('ROUTE_STATISTICS_MODEL', Visitor::class), + + /* + |-------------------------------------------------------------------------- + | Store Strategy + |-------------------------------------------------------------------------- + | + | If you set this to true, the Logs stored in the database using queues + | It is good when you have large database + | + */ + 'queued' => env('ROUTE_STATISTICS_QUEUED', false), + + /* + |-------------------------------------------------------------------------- + | User Model + |-------------------------------------------------------------------------- + | + | This is the model used for user relationships. + | You can set a custom user model for relationships. + | + | Leaving this empty will use the model from the 'users' auth provider. + | + */ + 'user_model' => env('ROUTE_STATISTICS_USER_MODEL'), +]; diff --git a/database/migrations/2026_01_06_112810_create_visitors_table.php b/database/migrations/2026_01_06_112810_create_visitors_table.php new file mode 100644 index 0000000..8cf546b --- /dev/null +++ b/database/migrations/2026_01_06_112810_create_visitors_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignIdFor(User::class)->nullable()->constrained()->cascadeOnDelete(); + $table->unsignedBigInteger('team_id')->nullable(); // Can be changed to the following if your application uses teams: $table->foreignId('team_id')->nullable()->constrained(); + $table->string('method')->nullable(); + $table->string('route')->nullable(); + $table->json('parameters')->nullable(); + $table->integer('status')->nullable(); + $table->ipAddress('ip')->nullable(); + $table->timestamp('date'); + $table->unsignedInteger('counter'); + + $table->index('date'); + $table->index(['user_id', 'date', 'route', 'method']); + $table->index(['team_id', 'date', 'route', 'method']); + $table->index(['route', 'method', 'date']); + }); + } + + public function down() + { + Schema::dropIfExists('visitors'); + } +}; diff --git a/resources/views/components/partials/footer.blade.php b/resources/views/components/partials/footer.blade.php index 2c070df..04475a2 100644 --- a/resources/views/components/partials/footer.blade.php +++ b/resources/views/components/partials/footer.blade.php @@ -82,16 +82,16 @@ class="col-12 col-xl-4 col-lg-4 col-sm-6 last-paragraph-no-margin text-xl-start
- Statistik Pengunjung + Statistik Visitor
diff --git a/routes/web.php b/routes/web.php index b9a30ea..1e2a66f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ name('home'); -Route::get('/about-us', AboutUs::class)->name('about-us'); +Route::middleware([TrackVisitor::class])->group(function () { + Route::get('/', Index::class)->name('home'); + Route::get('/about-us', AboutUs::class)->name('about-us'); -Route::prefix('news')->group(function () { - Route::get('/', NewsIndex::class)->name('news.index'); - Route::get('/{news:slug}', NewsShow::class)->name('news.show'); + Route::prefix('news')->group(function () { + Route::get('/', NewsIndex::class)->name('news.index'); + Route::get('/{news:slug}', NewsShow::class)->name('news.show'); + }); + + Route::get('/announcements', Announcement::class)->name('announcement'); }); - -Route::get('/announcements', Announcement::class)->name('announcement');