From 0f6dbb95e7bac2ecf40ba749cbe7adca905489e9 Mon Sep 17 00:00:00 2001 From: Bayu Lukman Yusuf Date: Tue, 3 Mar 2026 11:54:40 +0700 Subject: [PATCH 1/8] item visitor log --- app/Http/Controllers/ProductController.php | 49 +++- app/Models/AnalyticsProductVisit.php | 40 +++ .../AnalyticsProductVisitRepository.php | 255 ++++++++++++++++++ config/database.php | 13 + ..._create_analytics_product_visits_table.php | 47 ++++ 5 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 app/Models/AnalyticsProductVisit.php create mode 100644 app/Repositories/AnalyticsProductVisitRepository.php create mode 100644 database/migrations/2026_03_03_015321_create_analytics_product_visits_table.php diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 30afa56..87a0d0e 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -6,6 +6,7 @@ use App\Models\Gender; use App\Models\StoreCategory; use App\Repositories\Catalog\CategoryRepository; use App\Repositories\Catalog\GenderRepository; +use App\Repositories\AnalyticsProductVisitRepository; use App\Repositories\Catalog\ProductRepository; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; @@ -477,11 +478,57 @@ class ProductController extends Controller ]); } + /** + * Track product visit analytics + */ + private function trackProductVisit(int $productId, Request $request): void + { + try { + $analyticsRepository = new AnalyticsProductVisitRepository(); + + $visitData = [ + 'item_id' => $productId, + 'user_id' => auth()->id(), + 'session_id' => session()->getId(), + 'ip_address' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'started_at' => now(), + 'duration_seconds' => 0, // Will be updated when user leaves + 'device_type' => $this->getDeviceType($request->userAgent()), + ]; + + $analyticsRepository->create($visitData); + } catch (\Exception $e) { + // Log error but don't break the application + \Log::error('Failed to track product visit: ' . $e->getMessage()); + } + } + + /** + * Detect device type from user agent + */ + private function getDeviceType(string $userAgent): string + { + $userAgent = strtolower($userAgent); + + if (strpos($userAgent, 'mobile') !== false) { + return 'Mobile'; + } + + if (strpos($userAgent, 'tablet') !== false) { + return 'Tablet'; + } + + return 'Desktop'; + } + public function detailFashion($slug, Request $request, ProductRepository $productRepository) { - $product = $productRepository->show($slug); + // Track product visit analytics + $this->trackProductVisit($product->id, $request); + $complete_look_products_data = $productRepository->getList([ 'category_id' => $product->category1_id, 'limit' => 4, diff --git a/app/Models/AnalyticsProductVisit.php b/app/Models/AnalyticsProductVisit.php new file mode 100644 index 0000000..9647aa2 --- /dev/null +++ b/app/Models/AnalyticsProductVisit.php @@ -0,0 +1,40 @@ + 'datetime', + 'ended_at' => 'datetime', + ]; + + + public function calculateDuration(): void + { + if ($this->started_at && $this->ended_at) { + $this->duration_seconds = + $this->ended_at->diffInSeconds($this->started_at); + } + } + +} diff --git a/app/Repositories/AnalyticsProductVisitRepository.php b/app/Repositories/AnalyticsProductVisitRepository.php new file mode 100644 index 0000000..e9f1d1b --- /dev/null +++ b/app/Repositories/AnalyticsProductVisitRepository.php @@ -0,0 +1,255 @@ +where('item_id', $filters['item_id']); + } + + // Filter by user ID + if (isset($filters['user_id'])) { + $query->where('user_id', $filters['user_id']); + } + + // Filter by date range + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + // Filter by IP address + if (isset($filters['ip_address'])) { + $query->where('ip_address', $filters['ip_address']); + } + + return $query->orderBy('started_at', 'desc')->get(); + } + + /** + * Get product visits with pagination + */ + public function getPaginated(array $filters = [], int $perPage = 20): LengthAwarePaginator + { + $query = AnalyticsProductVisit::query(); + + // Apply same filters as getAll method + if (isset($filters['item_id'])) { + $query->where('item_id', $filters['item_id']); + } + + if (isset($filters['user_id'])) { + $query->where('user_id', $filters['user_id']); + } + + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + return $query->orderBy('started_at', 'desc') + ->paginate($perPage); + } + + /** + * Create a new product visit record + */ + public function create(array $data): AnalyticsProductVisit + { + return AnalyticsProductVisit::create($data); + } + + /** + * Update existing product visit + */ + public function update(int $id, array $data): bool + { + $visit = AnalyticsProductVisit::find($id); + + if (!$visit) { + return false; + } + + return $visit->update($data); + } + + /** + * Delete a product visit record + */ + public function delete(int $id): bool + { + $visit = AnalyticsProductVisit::find($id); + + if (!$visit) { + return false; + } + + return $visit->delete(); + } + + /** + * Get visit statistics + */ + public function getStatistics(array $filters = []): array + { + $query = AnalyticsProductVisit::query(); + + // Apply date filters + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + $totalVisits = $query->count(); + + $uniqueVisitors = $query->distinct('user_id')->count('user_id'); + + $averageDuration = $query->avg('duration_seconds'); + + $totalDuration = $query->sum('duration_seconds'); + + return [ + 'total_visits' => $totalVisits, + 'unique_visitors' => $uniqueVisitors, + 'average_duration_seconds' => round($averageDuration, 2), + 'total_duration_seconds' => $totalDuration, + 'average_duration_formatted' => $this->formatDuration($averageDuration), + 'total_duration_formatted' => $this->formatDuration($totalDuration), + ]; + } + + /** + * Get most visited products + */ + public function getMostVisitedProducts(int $limit = 10): array + { + return AnalyticsProductVisit::query() + ->selectRaw('item_id, COUNT(*) as visit_count, AVG(duration_seconds) as avg_duration') + ->groupBy('item_id') + ->orderBy('visit_count', 'desc') + ->limit($limit) + ->get() + ->toArray(); + } + + /** + * Get visits by device type + */ + public function getVisitsByDeviceType(array $filters = []): array + { + $query = AnalyticsProductVisit::query(); + + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + return $query->selectRaw('device_type, COUNT(*) as count') + ->groupBy('device_type') + ->orderBy('count', 'desc') + ->get() + ->toArray(); + } + + /** + * Get daily visit statistics + */ + public function getDailyStatistics(array $filters = []): array + { + $query = AnalyticsProductVisit::query(); + + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + return $query->selectRaw('DATE(started_at) as date, COUNT(*) as visits, COUNT(DISTINCT user_id) as unique_visitors') + ->groupBy('date') + ->orderBy('date', 'desc') + ->get() + ->toArray(); + } + + /** + * Format duration in human readable format + */ + private function formatDuration(float $seconds): string + { + if ($seconds < 60) { + return round($seconds) . 's'; + } + + if ($seconds < 3600) { + $minutes = floor($seconds / 60); + $remainingSeconds = $seconds % 60; + return $minutes . 'm ' . round($remainingSeconds) . 's'; + } + + $hours = floor($seconds / 3600); + $remainingSeconds = $seconds % 3600; + $minutes = floor($remainingSeconds / 60); + $remainingSeconds = $remainingSeconds % 60; + + return $hours . 'h ' . $minutes . 'm ' . round($remainingSeconds) . 's'; + } + + /** + * Get product visit by ID + */ + public function findById(int $id): ?AnalyticsProductVisit + { + return AnalyticsProductVisit::find($id); + } + + /** + * Get visits by session ID + */ + public function getBySessionId(string $sessionId): Collection + { + return AnalyticsProductVisit::where('session_id', $sessionId) + ->orderBy('started_at', 'desc') + ->get(); + } + + /** + * Bulk insert visits + */ + public function bulkInsert(array $visits): bool + { + try { + AnalyticsProductVisit::insert($visits); + return true; + } catch (\Exception $e) { + return false; + } + } +} diff --git a/config/database.php b/config/database.php index 42b3e25..450a8b6 100644 --- a/config/database.php +++ b/config/database.php @@ -112,6 +112,19 @@ return [ // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), ], + + + 'ecommerce' => [ + 'driver' => 'sqlite', + 'url' => env('DB_ECOMMERCE_URL'), + 'database' => env('DB_ECOMMERCE_DATABASE', database_path('ecommerce.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_ECOMMERCE_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + ], + ], /* diff --git a/database/migrations/2026_03_03_015321_create_analytics_product_visits_table.php b/database/migrations/2026_03_03_015321_create_analytics_product_visits_table.php new file mode 100644 index 0000000..ba23305 --- /dev/null +++ b/database/migrations/2026_03_03_015321_create_analytics_product_visits_table.php @@ -0,0 +1,47 @@ +create('analytics_product_visits', function (Blueprint $table) { + $table->id(); + + // Relasi dasar + $table->unsignedBigInteger('item_id')->index(); + $table->unsignedBigInteger('user_id')->nullable()->index(); + $table->string('session_id')->nullable()->index(); + + // Tracking waktu + $table->timestamp('started_at')->index(); + $table->timestamp('ended_at')->nullable(); + $table->integer('duration_seconds')->nullable(); + + // Optional analytics tambahan + $table->string('ip_address', 45)->nullable(); + $table->string('device_type')->nullable(); + $table->text('user_agent')->nullable(); + $table->string('referrer')->nullable(); + + $table->timestamps(); + + // Index tambahan untuk performa + $table->index(['item_id', 'started_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::connection('ecommerce')->dropIfExists('analytics_product_visits'); + } +}; From 06ab0f42a5a1064aef99524300acf1a294803ccb Mon Sep 17 00:00:00 2001 From: Bayu Lukman Yusuf Date: Tue, 3 Mar 2026 14:52:32 +0700 Subject: [PATCH 2/8] banner aspect ratio --- .../components/grocery/home-slider.blade.php | 81 +++++++++---------- resources/views/home/grocery.blade.php | 17 ++-- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/resources/views/components/grocery/home-slider.blade.php b/resources/views/components/grocery/home-slider.blade.php index d945102..a940c41 100644 --- a/resources/views/components/grocery/home-slider.blade.php +++ b/resources/views/components/grocery/home-slider.blade.php @@ -1,50 +1,47 @@
-
-
+
+
+
- @foreach ($items as $item) -
- {{--
-
-
-
-

🔥 Free shipping - order over - 50$ -

-

Healthy Food Available to Everyone

- Shop now + @foreach ($items as $item) +
+ {{--
+
+
+
+

🔥 Free shipping - order over + 50$ +

+

Healthy Food Available to Everyone

+ Shop now +
-
-
--}} - Image -
- @endforeach +
--}} + Image +
+ @endforeach +
+ + +
- - -
-
-
-
-
-
diff --git a/resources/views/home/grocery.blade.php b/resources/views/home/grocery.blade.php index 0b64b46..42d1ac3 100644 --- a/resources/views/home/grocery.blade.php +++ b/resources/views/home/grocery.blade.php @@ -1,8 +1,6 @@ @extends('layouts.landing', ['title' => config('app.tagline')]) @section('content') - - @@ -26,7 +24,7 @@ - + @@ -166,12 +164,12 @@ class="d-flex flex-column flex-sm-row align-items-center h-100 bg-body-tertiary rounded-5 overflow-hidden">

Make online shop easier with our AsiaGolf App

-
- + - + @@ -191,7 +189,8 @@ d="M24.575 9.45c-2.184 0-3.914 1.704-3.914 4.071 0 2.272 1.729 4.071 3.914 4.071s3.914-1.704 3.914-4.071c0-2.461-1.729-4.071-3.914-4.071zm0 6.438c-1.183 0-2.184-1.041-2.184-2.462s1.001-2.462 2.184-2.462 2.184.947 2.184 2.462c0 1.42-1.001 2.462-2.184 2.462zM16.11 9.45c-2.184 0-3.914 1.704-3.914 4.071 0 2.272 1.729 4.071 3.914 4.071s3.914-1.704 3.914-4.071c0-2.461-1.729-4.071-3.914-4.071zm0 6.438c-1.183 0-2.184-1.041-2.184-2.462s1.001-2.462 2.184-2.462 2.184.947 2.184 2.462c0 1.42-1.001 2.462-2.184 2.462zM6.007 10.681v1.704h3.914c-.091.947-.455 1.704-.91 2.177-.546.568-1.456 1.231-3.004 1.231-2.457 0-4.278-1.988-4.278-4.544s1.911-4.544 4.278-4.544c1.274 0 2.275.568 3.004 1.231l1.183-1.231C9.193 5.757 7.918 5 6.098 5 2.822 5 0 7.84 0 11.249s2.822 6.249 6.098 6.249c1.82 0 3.095-.568 4.187-1.799 1.092-1.136 1.456-2.745 1.456-3.976 0-.379 0-.757-.091-1.041H6.007zm41.322 1.325c-.364-.947-1.274-2.556-3.277-2.556s-3.641 1.61-3.641 4.071c0 2.272 1.638 4.071 3.823 4.071 1.729 0 2.822-1.136 3.186-1.799l-1.274-.947c-.455.663-1.001 1.136-1.911 1.136s-1.456-.379-1.911-1.231l5.188-2.272-.182-.473zm-5.279 1.326c0-1.515 1.183-2.367 2.002-2.367.637 0 1.274.379 1.456.852l-3.459 1.515zm-4.278 3.882h1.729V5.379h-1.729v11.834zm-2.73-6.911c-.455-.473-1.183-.947-2.093-.947-1.911 0-3.732 1.799-3.732 4.071s1.729 3.976 3.732 3.976c.91 0 1.638-.473 2.002-.947h.091v.568c0 1.515-.819 2.367-2.093 2.367-1.001 0-1.729-.757-1.911-1.42l-1.456.663C30.036 19.675 31.128 21 33.039 21c2.002 0 3.641-1.231 3.641-4.166V9.639h-1.638v.663zm-2.002 5.586c-1.183 0-2.184-1.041-2.184-2.462s1.001-2.462 2.184-2.462 2.093 1.041 2.093 2.462-.91 2.462-2.093 2.462zM55.247 5.379h-4.096v11.834h1.729v-4.45h2.367c1.911 0 3.732-1.42 3.732-3.692s-1.82-3.692-3.732-3.692zm.091 5.681h-2.457V6.988h2.457c1.274 0 2.002 1.136 2.002 1.988-.091 1.041-.819 2.083-2.002 2.083zm10.467-1.704c-1.274 0-2.549.568-3.004 1.799l1.547.663c.364-.663.91-.852 1.547-.852.91 0 1.729.568 1.82 1.515v.095c-.273-.189-1.001-.473-1.729-.473-1.638 0-3.277.947-3.277 2.651 0 1.609 1.365 2.651 2.821 2.651 1.183 0 1.729-.568 2.184-1.136h.091v.947h1.638v-4.544c-.182-2.083-1.729-3.314-3.641-3.314zm-.182 6.533c-.546 0-1.365-.284-1.365-1.041 0-.947 1.001-1.231 1.82-1.231.728 0 1.092.189 1.547.379-.182 1.136-1.092 1.894-2.002 1.894zm9.557-6.249l-1.911 5.112h-.091l-2.002-5.112h-1.82l3.004 7.195-1.729 3.976h1.729L77 9.639h-1.82zm-15.291 7.574h1.729V5.379h-1.729v11.834z" /> - Date: Tue, 3 Mar 2026 15:01:31 +0700 Subject: [PATCH 3/8] popular grid 6 xxl screen --- .../views/components/grocery/popular-products.blade.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/views/components/grocery/popular-products.blade.php b/resources/views/components/grocery/popular-products.blade.php index 059483b..a4a200e 100644 --- a/resources/views/components/grocery/popular-products.blade.php +++ b/resources/views/components/grocery/popular-products.blade.php @@ -1,8 +1,8 @@ -
+
-
+

{{ __('home_popular_products.category') }}

-
+

{{ __('home_popular_products.title') }}

-
+
@foreach ($products as $key => $value) From cecc894944171cece9f3b1742360991ef13c6f77 Mon Sep 17 00:00:00 2001 From: Bayu Lukman Yusuf Date: Tue, 3 Mar 2026 15:09:10 +0700 Subject: [PATCH 4/8] home category full width --- .../components/grocery/featured-categories.blade.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/resources/views/components/grocery/featured-categories.blade.php b/resources/views/components/grocery/featured-categories.blade.php index cbccf6e..ae7cd27 100644 --- a/resources/views/components/grocery/featured-categories.blade.php +++ b/resources/views/components/grocery/featured-categories.blade.php @@ -1,4 +1,4 @@ -
+
From 3e7a62be4463972171086015668bdd6824cc8dc4 Mon Sep 17 00:00:00 2001 From: Bayu Lukman Yusuf Date: Tue, 3 Mar 2026 15:34:04 +0700 Subject: [PATCH 5/8] special product ful width --- .../components/grocery/special-products.blade.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/resources/views/components/grocery/special-products.blade.php b/resources/views/components/grocery/special-products.blade.php index 6e2e00a..55bde0f 100644 --- a/resources/views/components/grocery/special-products.blade.php +++ b/resources/views/components/grocery/special-products.blade.php @@ -1,8 +1,8 @@ -
+
-
+
@@ -22,7 +22,7 @@
-
+

{{ __('special_product.title') }}