+``` + +## π Resource Links + +- [Tailwind Responsive Design](https://tailwindcss.com/docs/responsive-design) +- [Chart.js Responsive](https://www.chartjs.org/docs/latest/general/responsive.html) +- [Mobile-First Design Patterns](https://developer.mozilla.org/en-US/docs/Mobile/Viewport_meta_tag) + +--- + +**Last Updated**: May 7, 2026 +**Status**: β Optimasi Responsive Complete diff --git a/TESTING_MOBILE_RESPONSIVE.md b/TESTING_MOBILE_RESPONSIVE.md new file mode 100644 index 0000000..4cbe367 --- /dev/null +++ b/TESTING_MOBILE_RESPONSIVE.md @@ -0,0 +1,349 @@ +# π± Panduan Testing Mobile Responsive di DevTools + +## π Cara Test Dashboard Responsive dengan Device Emulation + +### **Langkah 1: Buka Dashboard Admin** +``` +1. Buka browser (Chrome, Firefox, Edge) +2. Akses: http://localhost:8000/admin/dashboard +3. Login dengan: admin@gmail.com / admin123 +``` + +### **Langkah 2: Buka DevTools** +``` +Windows/Linux: Tekan Ctrl + Shift + I +Mac: Tekan Cmd + Option + I +Atau: Klik kanan β Inspect +``` + +### **Langkah 3: Aktifkan Device Toolbar (Mobile Mode)** +``` +Windows/Linux: Tekan Ctrl + Shift + M +Mac: Tekan Cmd + Shift + M +Atau: DevTools β Click icon "Toggle device toolbar" +``` + +--- + +## π± Device Emulation yang Perlu Ditest + +### **1. Mobile Phones (Portrait)** +| Device | Resolution | Size | +|--------|-----------|------| +| iPhone SE | 375Γ667 | Kecil | +| iPhone 12 | 390Γ844 | Medium | +| iPhone 13 Pro | 390Γ844 | Medium | +| iPhone 14 | 390Γ844 | Medium | +| Pixel 5 | 393Γ851 | Medium | +| Galaxy S21 | 360Γ800 | Kecil | + +### **2. Tablet (Portrait & Landscape)** +| Device | Portrait | Landscape | +|--------|----------|-----------| +| iPad Air | 768Γ1024 | 1024Γ768 | +| iPad Pro | 1024Γ1366 | 1366Γ1024 | +| Galaxy Tab | 600Γ960 | 960Γ600 | + +### **3. Desktop** +| Resolution | Size | +|-----------|------| +| 1366Γ768 | Laptop | +| 1920Γ1080 | Desktop Full HD | +| 2560Γ1440 | Desktop 2K | + +--- + +## β Checklist Testing Mobile Mode + +### **Header/Navigation** β +- [ ] Hamburger menu (β‘) **VISIBLE** di mobile +- [ ] Hamburger menu **HIDDEN** di tablet+ (md breakpoint) +- [ ] Dark mode toggle **VISIBLE** di semua ukuran +- [ ] Logout button **VISIBLE** tapi text hidden di mobile +- [ ] Page title **VISIBLE** di mobile + +### **Sidebar** β +- [ ] Sidebar **HIDDEN** di mobile (default) +- [ ] Hamburger menu bisa **membuka** sidebar +- [ ] Sidebar bisa **ditutup** dengan overlay +- [ ] Sidebar link bisa **ditutup** otomatis saat diklik (mobile) +- [ ] Sidebar **VISIBLE** di tablet+ (md breakpoint) + +### **Main Content** β +- [ ] Grid layout **2 kolom** di mobile +- [ ] Grid layout **2 kolom** di tablet +- [ ] Grid layout **4 kolom** di desktop +- [ ] **No horizontal scroll** di mobile +- [ ] Text **readable** di semua ukuran +- [ ] Padding/spacing **optimal** per device + +### **Statistics Cards** β +- [ ] Card height **responsive** +- [ ] Card padding **compact** di mobile +- [ ] Card padding **full** di desktop +- [ ] Icon size **scalable** (8px β 10px) +- [ ] Statistics text **readable** + +### **Charts** β +- [ ] Chart height **250px** di mobile +- [ ] Chart height **320px** di desktop +- [ ] Chart font size **10px** di mobile +- [ ] Chart font size **12px** di desktop +- [ ] Chart point radius **3px** di mobile +- [ ] Chart point radius **5px** di desktop +- [ ] Chart **tidak terpotong** (no overflow) + +### **Tables** β +- [ ] Table kolom **hide/show** sesuai breakpoint +- [ ] Mobile: Hanya **Nama + Status** visible +- [ ] Tablet: **Nama, Email, Status** visible +- [ ] Desktop: **Semua kolom** visible +- [ ] Thumbnail image **responsive** (8px β 12px) +- [ ] No horizontal scroll di mobile + +### **Forms/Modals** β +- [ ] Modal **full width** di mobile +- [ ] Modal **centered + max-width** di desktop +- [ ] Input fields **full width** di mobile +- [ ] Buttons **properly spaced** (44px min height) +- [ ] Form **accessible** dan tidak terjepit + +--- + +## π Testing Steps Detail + +### **Test 1: Mobile Layout (375px)** +``` +1. DevTools β Device Emulation β iPhone 12 (390Γ844) +2. Refresh page (Ctrl+R) +3. Verify: + β Sidebar hidden + β Hamburger menu visible + β 2 kolom grid + β Text readable + β No horizontal scroll +``` + +### **Test 2: Hamburger Menu Toggle** +``` +1. Mobile mode (375px) +2. Klik hamburger menu (β‘) di header kiri +3. Verify: + β Sidebar slide dari kiri + β Overlay gelap muncul + β Sidebar tertutup saat klik link + β Sidebar tertutup saat klik overlay +``` + +### **Test 3: Resize from Mobile to Desktop** +``` +1. DevTools β Device Emulation β iPhone 12 (390Γ844) +2. Refresh page +3. Verifikasi mobile layout (sidebar hidden, hamburger visible) +4. Resize browser ke 768px (md breakpoint) +5. Verify: + β Sidebar auto-appear + β Hamburger auto-hide + β Layout adjust to 2 columns +``` + +### **Test 4: Resize from Desktop to Mobile** +``` +1. DevTools β Responsive (1920Γ1080) +2. Refresh page +3. Verifikasi desktop layout (sidebar visible, 4 columns) +4. Resize browser ke 374px (mobile) +5. Verify: + β Sidebar auto-hide + β Hamburger auto-show + β Layout adjust to 2 columns +``` + +### **Test 5: Tablet Landscape** +``` +1. DevTools β Device Emulation β iPad Air (Landscape 1024Γ768) +2. Refresh page +3. Verify: + β Sidebar visible (md: breakpoint) + β Hamburger hidden + β 2-4 kolom grid (bergantung desain) + β All content visible +``` + +### **Test 6: Chart Responsiveness** +``` +1. Mobile mode (375px) +2. Buka Dashboard atau System Statistics +3. Verify chart: + β Height 250px + β Font size small + β Point radius 3px +4. Resize ke desktop (1920px) +5. Verify chart: + β Height 320px + β Font size normal + β Point radius 5px +``` + +### **Test 7: All Pages Responsive** +``` +Testing pages: +1. Dashboard (/) + β Stat cards responsive + β Charts responsive + +2. System Statistics (/system-statistics) + β Charts visible + β No overlap + +3. Classification History (/classification-history) + β Table responsive + β Columns hide/show + +4. Manage Admin (/manage-admin) + β Table responsive + β Modal adaptive +``` + +--- + +## π Troubleshooting + +### **Problem: Sidebar tidak hilang di mobile** +``` +Solution: +1. Hard refresh: Ctrl+Shift+R (bukan Ctrl+R) +2. Clear DevTools: Tekan X di kanan atas DevTools +3. Buka file app.blade.php +4. Cek class: -translate-x-full pada sidebar-wrapper +5. Cek viewport meta tag ada: +``` + +### **Problem: Hamburger menu tidak berfungsi** +``` +Solution: +1. Buka DevTools Console (F12 β Console) +2. Verify tidak ada error (lihat warna merah) +3. Test manual: Ketik di console: + document.getElementById('menuToggle').click() +4. Klik hamburger di page, sidebar harus slide +``` + +### **Problem: Chart tidak responsive** +``` +Solution: +1. Buka file dengan chart (dashboard.blade.php) +2. Cek ada script: + const isMobile = window.innerWidth < 768; + Chart.js options dengan conditional values +3. Refresh page, cek di DevTools Console +``` + +### **Problem: Grid tidak berubah kolom** +``` +Solution: +1. Cek class pada grid: + grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 +2. Hard refresh: Ctrl+Shift+R +3. Inspect element (F12) +4. Di DevTools, cek computed styles +5. Verifikasi breakpoint md (768px) dan lg (1024px) +``` + +### **Problem: Text terlalu kecil/besar** +``` +Solution: +1. Cek responsive text classes: + text-xs md:text-sm lg:text-base +2. Inspect element untuk lihat actual font-size +3. Browser zoom tidak mempengaruhi +4. Refresh page untuk reset +``` + +--- + +## π Console Testing Commands + +Paste di DevTools Console untuk cek: + +```javascript +// 1. Check viewport width +console.log('Viewport:', window.innerWidth, 'x', window.innerHeight); + +// 2. Check breakpoints +console.log('Mobile (< 768px):', window.innerWidth < 768); +console.log('Tablet (β₯ 768px):', window.innerWidth >= 768); +console.log('Desktop (β₯ 1024px):', window.innerWidth >= 1024); + +// 3. Check responsive elements +console.log('Sidebar visible:', !document.getElementById('sidebar-wrapper').classList.contains('-translate-x-full')); +console.log('Hamburger visible:', window.getComputedStyle(document.getElementById('menuToggle')).display !== 'none'); + +// 4. Manually toggle sidebar +document.getElementById('menuToggle').click(); + +// 5. Check all classes +console.log('Sidebar classes:', document.getElementById('sidebar-wrapper').className); +console.log('Overlay classes:', document.getElementById('sidebar-overlay').className); +``` + +--- + +## β¨ Expected Results + +### **Mobile (390px)** +``` +β Sidebar: Hidden (-translate-x-full) +β Hamburger: Visible (md:hidden removed) +β Grid: 2 columns (grid-cols-2) +β Gap: Small (gap-3) +β Padding: Compact (p-3) +β Font: Small (text-xs) +β Charts: 250px height +β No horizontal scroll +``` + +### **Tablet (768px)** +``` +β Sidebar: Visible (md:translate-x-0) +β Hamburger: Hidden (md:hidden active) +β Grid: 2-4 columns adaptive +β Gap: Medium (gap-3/4) +β Padding: Medium (p-4/6) +β Font: Base (text-sm) +β Charts: 250-300px height +``` + +### **Desktop (1920px)** +``` +β Sidebar: Visible +β Hamburger: Hidden +β Grid: 4 columns (lg:grid-cols-4) +β Gap: Large (gap-6) +β Padding: Full (p-6) +β Font: Large (text-base) +β Charts: 320px height +``` + +--- + +## π― Final Verification + +Sebelum launch production: + +- [ ] Semua page tested di 5+ resolusi +- [ ] Hamburger menu works di mobile +- [ ] Sidebar toggle works properly +- [ ] Charts responsive di semua ukuran +- [ ] Tables adaptive columns work +- [ ] No console errors +- [ ] No horizontal scroll mobile +- [ ] Touch targets 44px+ size +- [ ] Performance metrics OK +- [ ] Tested di Chrome, Firefox, Safari + +--- + +**Status:** β READY TO TEST +**Last Updated:** May 7, 2026 +**Version:** 1.0 diff --git a/app/Http/Controllers/TomatController.php b/app/Http/Controllers/TomatController.php index f8e8dcc..df9dfef 100644 --- a/app/Http/Controllers/TomatController.php +++ b/app/Http/Controllers/TomatController.php @@ -36,12 +36,27 @@ public function classify(Request $request) ) ->post($this->apiUrl); - if ($response->failed()) { - return back()->with('error', 'Gagal menghubungi API klasifikasi. Status: ' . $response->status()); - } - + $result = $response->json(); + // Jika gambar bukan tomat / confidence rendah +if ($response->status() == 422) { + + return back()->with( + 'error', + $result['message'] ?? 'Gambar bukan tomat' + ); +} + +// Jika API benar-benar gagal +if ($response->failed()) { + + return back()->with( + 'error', + 'Gagal menghubungi API klasifikasi. Status: ' . $response->status() + ); +} + if (!isset($result['success']) || !$result['success']) { $errorMsg = $result['message'] ?? 'Prediksi gagal'; return back()->with('error', 'Error dari API: ' . $errorMsg); diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php index cdfb013..66d0d52 100644 --- a/app/Http/Controllers/UploadController.php +++ b/app/Http/Controllers/UploadController.php @@ -131,19 +131,29 @@ private function sendToFlaskAPI($file) ) ->post($this->flaskApiUrl); - if ($response->failed()) { - if ($attempt < $maxRetries + 1) { - usleep($retryDelay * 1000); - continue; - } - return [ - 'success' => false, - 'error' => 'Gagal menghubungi API klasifikasi. Status: ' . $response->status() - ]; - } - $result = $response->json(); + // Jika gambar bukan tomat / confidence rendah + if ($response->status() == 422) { + + return [ + 'success' => false, + 'error' => $result['message'] ?? 'Gambar bukan tomat' + ]; + } + if ($response->failed()) { + + if ($attempt < $maxRetries + 1) { + usleep($retryDelay * 1000); + continue; + } + + return [ + 'success' => false, + 'error' => 'Gagal menghubungi API klasifikasi. Status: ' . $response->status() + ]; +} + // Validate API response structure if (!isset($result['success'])) { return [ @@ -281,14 +291,22 @@ public function adminLogin(Request $request) // Verifikasi password dan pastikan role adalah 'admin' if ($user && \Hash::check($password, $user->password) && $user->role === 'admin') { - // Simpan session + // Regenerate session untuk security + $request->session()->regenerate(); + + // Simpan session admin session([ 'admin_logged_in' => true, 'admin_user_id' => $user->id, - 'admin_name' => $user->name + 'admin_name' => $user->name, + 'admin_email' => $user->email ]); - return redirect()->route('admin.dashboard')->with('success', 'Login berhasil! Selamat datang, ' . $user->name); + // Save session explicitly to ensure persistence + session()->save(); + + return redirect()->route('admin.dashboard') + ->with('success', 'Login berhasil! Selamat datang, ' . $user->name); } // Login gagal diff --git a/app/Http/Middleware/AdminAuth.php b/app/Http/Middleware/AdminAuth.php new file mode 100644 index 0000000..370ab6d --- /dev/null +++ b/app/Http/Middleware/AdminAuth.php @@ -0,0 +1,35 @@ +wantsJson()) { + return response()->json([ + 'error' => 'Unauthorized - Silakan login terlebih dahulu' + ], 403); + } + + // Otherwise redirect to login page + return redirect()->route('admin.login') + ->with('error', 'Silakan login sebagai admin terlebih dahulu'); + } + + return $next($request); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..835a379 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,7 +11,10 @@ health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + // Register admin auth middleware + $middleware->alias([ + 'admin.auth' => \App\Http\Middleware\AdminAuth::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..bf3e538 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,120 @@ +// Service Worker untuk caching dan offline support +const CACHE_NAME = 'admin-dashboard-v1'; +const urlsToCache = [ + '/admin', + '/admin/dashboard', + '/admin/system-statistics', + '/admin/classification-history', + '/admin/manage-admin', + 'https://cdn.tailwindcss.com', + 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', +]; + +// Install Service Worker +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => { + console.log('Cache opened'); + return cache.addAll(urlsToCache).catch(err => { + console.log('Cache addAll partially failed:', err); + // Continue even if some urls fail to cache + return Promise.resolve(); + }); + }) + ); + self.skipWaiting(); +}); + +// Activate Service Worker +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheName !== CACHE_NAME) { + console.log('Deleting old cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }) + ); + self.clients.claim(); +}); + +// Fetch Event - Network First, then Cache +self.addEventListener('fetch', event => { + const { request } = event; + const url = new URL(request.url); + + // Skip non-GET requests + if (request.method !== 'GET') { + return; + } + + // Skip admin area fetches (always fresh) + if (url.pathname.includes('/admin/api/')) { + event.respondWith( + fetch(request) + .then(response => { + // Cache valid responses + if (response && response.status === 200 && response.type === 'basic') { + const responseToCache = response.clone(); + caches.open(CACHE_NAME).then(cache => { + cache.put(request, responseToCache); + }); + } + return response; + }) + .catch(() => { + // Return cached version if network fails + return caches.match(request) + .then(cached => cached || new Response('Offline', { status: 503 })); + }) + ); + return; + } + + // Cache First strategy untuk static assets + if (request.url.includes('cdn.') || + request.url.includes('fonts.') || + request.url.includes('cdnjs')) { + event.respondWith( + caches.match(request) + .then(cached => { + return cached || fetch(request) + .then(response => { + if (response && response.status === 200) { + const responseToCache = response.clone(); + caches.open(CACHE_NAME).then(cache => { + cache.put(request, responseToCache); + }); + } + return response; + }) + .catch(() => new Response('Resource not available', { status: 404 })); + }) + ); + return; + } + + // Network First for HTML pages + event.respondWith( + fetch(request) + .then(response => { + if (response && response.status === 200) { + const responseToCache = response.clone(); + caches.open(CACHE_NAME).then(cache => { + cache.put(request, responseToCache); + }); + } + return response; + }) + .catch(() => { + return caches.match(request) + .then(cached => cached || new Response('Offline - Page not cached', { status: 503 })); + }) + ); +}); diff --git a/resources/views/Admin/classification-history.blade.php b/resources/views/Admin/classification-history.blade.php index 6aae535..61de236 100644 --- a/resources/views/Admin/classification-history.blade.php +++ b/resources/views/Admin/classification-history.blade.php @@ -4,83 +4,84 @@ @section('page-title', 'Riwayat Klasifikasi') @section('content') -
Lihat semua riwayat klasifikasi tomat yang telah dilakukan
+Lihat semua riwayat klasifikasi tomat yang telah dilakukan