commit 2f8810b5262d9263cf2d0a15fbd676c34f3e4ca6 Author: EkiNanda57 <158057311+EkiNanda57@users.noreply.github.com> Date: Wed Jun 17 21:18:24 2026 +0700 Proyek Tugas Akhir diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..35db1dd --- /dev/null +++ b/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7cf1fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +/auth.json +/.fleet +/.idea +/.nova +/.vscode +/.zed diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a4c26b --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +
+ + + +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com/)** +- **[Tighten Co.](https://tighten.co)** +- **[WebReinvent](https://webreinvent.com/)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel/)** +- **[Cyber-Duck](https://cyber-duck.co.uk)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Jump24](https://jump24.co.uk)** +- **[Redberry](https://redberry.international/laravel/)** +- **[Active Logic](https://activelogic.com)** +- **[byte5](https://byte5.de)** +- **[OP.GG](https://op.gg)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 0000000..17a6e0f --- /dev/null +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,48 @@ +authenticate(); + $request->session()->regenerate(); + + // Ganti dari redirect()->intended(RouteServiceProvider::HOME) + // menjadi: + return redirect()->intended(route('klasifikasi')); + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request): RedirectResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php new file mode 100644 index 0000000..712394a --- /dev/null +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -0,0 +1,40 @@ +validate([ + 'email' => $request->user()->email, + 'password' => $request->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + $request->session()->put('auth.password_confirmed_at', time()); + + return redirect()->intended(route('dashboard', absolute: false)); + } +} diff --git a/app/Http/Controllers/Auth/DirectPasswordResetController.php b/app/Http/Controllers/Auth/DirectPasswordResetController.php new file mode 100644 index 0000000..fbbe2b7 --- /dev/null +++ b/app/Http/Controllers/Auth/DirectPasswordResetController.php @@ -0,0 +1,85 @@ +validate([ + 'email' => ['required', 'email'] + ], [ + 'email.required' => 'Email wajib diisi.', + 'email.email' => 'Format email tidak valid.' + ]); + + $user = User::where('email', $request->email)->first(); + + if (!$user) { + return back()->withErrors([ + 'email' => 'Email tidak ditemukan dalam sistem.' + ])->withInput(); + } + + // Simpan email ke session, redirect ke form password baru + session(['reset_email' => $request->email]); + return redirect()->route('password.direct.form'); + } + + // Langkah 3: Tampil form password baru + public function showResetForm() + { + if (!session('reset_email')) { + return redirect()->route('password.request') + ->withErrors(['email' => 'Sesi tidak valid, silakan ulangi.']); + } + return view('auth.reset-password-direct'); + } + + // Langkah 4: Simpan password baru + public function resetPassword(Request $request) + { + $request->validate([ + 'password' => ['required', 'confirmed', 'min:8'] + ], [ + 'password.required' => 'Password wajib diisi.', + 'password.confirmed' => 'Konfirmasi password tidak cocok.', + 'password.min' => 'Password minimal 8 karakter.' + ]); + + $email = session('reset_email'); + + if (!$email) { + return redirect()->route('password.request') + ->withErrors(['email' => 'Sesi tidak valid, silakan ulangi.']); + } + + $user = User::where('email', $email)->first(); + + if (!$user) { + return redirect()->route('password.request') + ->withErrors(['email' => 'User tidak ditemukan.']); + } + + $user->password = Hash::make($request->password); + $user->save(); + + // Hapus session + session()->forget('reset_email'); + + return redirect()->route('login') + ->with('status', 'Password berhasil diubah! Silakan masuk dengan password baru.'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 0000000..f64fa9b --- /dev/null +++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,24 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(route('dashboard', absolute: false)); + } + + $request->user()->sendEmailVerificationNotification(); + + return back()->with('status', 'verification-link-sent'); + } +} diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php new file mode 100644 index 0000000..ee3cb6f --- /dev/null +++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -0,0 +1,21 @@ +user()->hasVerifiedEmail() + ? redirect()->intended(route('dashboard', absolute: false)) + : view('auth.verify-email'); + } +} diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000..e8368bd --- /dev/null +++ b/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,62 @@ + $request]); + } + + /** + * Handle an incoming new password request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'token' => ['required'], + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function (User $user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + return $status == Password::PASSWORD_RESET + ? redirect()->route('login')->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php new file mode 100644 index 0000000..6916409 --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordController.php @@ -0,0 +1,29 @@ +validateWithBag('updatePassword', [ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('status', 'password-updated'); + } +} diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000..bf1ebfa --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,44 @@ +validate([ + 'email' => ['required', 'email'], + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $request->only('email') + ); + + return $status == Password::RESET_LINK_SENT + ? back()->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 0000000..6cd6986 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,50 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + ]); + + event(new Registered($user)); + + Auth::login($user); + + return redirect(route('klasifikasi', absolute: false)); + } +} diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..784765e --- /dev/null +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,27 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + } +} diff --git a/app/Http/Controllers/ClassificationController.php b/app/Http/Controllers/ClassificationController.php new file mode 100644 index 0000000..a00a93d --- /dev/null +++ b/app/Http/Controllers/ClassificationController.php @@ -0,0 +1,35 @@ + Dihapus karena tidak butuh simpan riwayat lagi +use App\Models\Image; +use Illuminate\Support\Facades\Http; + +class ClassificationController extends Controller +{ + public function classify($image_id) + { + // 1. Ambil gambar yang baru saja diupload + $image = Image::findOrFail($image_id); + + // 2. Path gambar di storage + $path = storage_path('app/public/' . $image->image_path); + + // 3. Kirim gambar ke API Python + $response = Http::attach( + 'image', + file_get_contents($path), + basename($path) + )->post('http://127.0.0.1:5000/predict'); + + $result = $response->json(); + + // KODE SIMPAN KE DATABASE (Classification::create) SUDAH DIHAPUS DI SINI + + // 4. Langsung kembalikan hasil ke halaman view (blade) + // Gunakan ->with('result', $result) agar datanya bisa ditampilkan di frontend + return redirect()->back()->with('success', 'Klasifikasi berhasil')->with('result', $result); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +where('user_id', Auth::id()) + ->whereNotNull('user_id') + ->whereHas('classification') + ->latest() + ->paginate(10); + + return view('history.history', compact('histories')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php new file mode 100644 index 0000000..9e2d21f --- /dev/null +++ b/app/Http/Controllers/ImageController.php @@ -0,0 +1,119 @@ +validate([ + 'image' => 'required|image|mimes:jpg,jpeg,png|max:2048' + ], [ + 'image.required' => 'Gambar insang ikan wajib diunggah.', + 'image.image' => 'File harus berupa gambar.', + 'image.mimes' => 'Format yang diizinkan hanya JPG, JPEG, atau PNG.', + 'image.max' => 'Ukuran gambar maksimal 2MB.' + ]); + + // ====================================== + // HAPUS FOTO LAMA — hanya untuk guest + // ====================================== + if (!Auth::check()) { + $fotoGuest = Image::whereNull('user_id')->get(); + foreach ($fotoGuest as $fotoLama) { + \Storage::delete('public/' . $fotoLama->image_path); + $fotoLama->delete(); + } + } + + // Simpan gambar baru + $imageFile = $request->file('image'); + $path = $imageFile->store('images', 'public'); + + // ============================================= + // Simpan user_id kalau login, null kalau guest + // ============================================= + $image = Image::create([ + 'user_id' => Auth::id(), // otomatis null kalau belum login + 'image_path' => $path + ]); + + $image_url = asset('storage/' . $path); + + // ========================================== + // KONEKSI KE API PYTHON (FLASK) + // ========================================== + try { + $apiUrl = 'http://127.0.0.1:5000/predict'; + + $response = Http::timeout(60)->attach( + 'image', + file_get_contents($imageFile->getRealPath()), + $imageFile->getClientOriginalName() + )->post($apiUrl); + + if ($response->successful()) { + + $result = $response->json(); + + // BUKAN INSANG + if ($result['status'] == 'bukan_insang') { + return redirect()->back()->with([ + 'error_insang' => $result['pesan'], + 'image_url' => $image_url + ]); + } + + // HASIL NORMAL + if ($result['status'] == 'ok') { + Classification::create([ + 'image_id' => $image->id, + 'kelas' => $result['prediksi_2kelas'], + 'prob_fresh' => $result['prob_fresh'], + 'tingkat_kesegaran' => $result['tingkat_kesegaran'] + ]); + + return redirect()->back()->with([ + 'success' => 'Gambar berhasil dianalisis oleh model AI!', + 'kelas' => $result['prediksi_2kelas'], + 'prob_fresh' => $result['prob_fresh'], + 'tingkat_kesegaran' => $result['tingkat_kesegaran'], + 'image_url' => $image_url, + 'cropped_image' => $result['cropped_image'] ?? null + ]); + } + + } else { + return redirect()->back()->withErrors([ + 'image' => 'Terjadi kesalahan pada API: ' . $response->body() + ]); + } + + } catch (\Exception $e) { + return redirect()->back()->withErrors([ + 'image' => 'Gagal terhubung ke server AI (Flask).' + ]); + } + } + + public function history() + { + $histories = Image::where('user_id', Auth::id()) + ->with('classification') + ->latest() + ->paginate(10); + + return view('history.history', compact('histories')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php new file mode 100644 index 0000000..a48eb8d --- /dev/null +++ b/app/Http/Controllers/ProfileController.php @@ -0,0 +1,60 @@ + $request->user(), + ]); + } + + /** + * Update the user's profile information. + */ + public function update(ProfileUpdateRequest $request): RedirectResponse + { + $request->user()->fill($request->validated()); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + $request->user()->save(); + + return Redirect::route('profile.edit')->with('status', 'profile-updated'); + } + + /** + * Delete the user's account. + */ + public function destroy(Request $request): RedirectResponse + { + $request->validateWithBag('userDeletion', [ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + + Auth::logout(); + + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return Redirect::to('/'); + } +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..2574642 --- /dev/null +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,85 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function authenticate(): void + { + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + */ + public function throttleKey(): string + { + return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); + } +} diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php new file mode 100644 index 0000000..3622a8f --- /dev/null +++ b/app/Http/Requests/ProfileUpdateRequest.php @@ -0,0 +1,30 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'lowercase', + 'email', + 'max:255', + Rule::unique(User::class)->ignore($this->user()->id), + ], + ]; + } +} diff --git a/app/Models/Classification.php b/app/Models/Classification.php new file mode 100644 index 0000000..23b7fd1 --- /dev/null +++ b/app/Models/Classification.php @@ -0,0 +1,20 @@ +belongsTo(Image::class); + } +} diff --git a/app/Models/Image.php b/app/Models/Image.php new file mode 100644 index 0000000..1fca58c --- /dev/null +++ b/app/Models/Image.php @@ -0,0 +1,23 @@ +belongsTo(User::class); + } + + public function classification() + { + return $this->hasOne(Classification::class); + } +} \ No newline at end of file diff --git a/app/Models/Prediction.php b/app/Models/Prediction.php new file mode 100644 index 0000000..f5a8692 --- /dev/null +++ b/app/Models/Prediction.php @@ -0,0 +1,20 @@ +belongsTo(Image::class); + } +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..749c7b7 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,48 @@ + */ + use HasFactory, Notifiable; + + /** + * The attributes that are mass assignable. + * + * @var list+ Masukkan email yang terdaftar, lalu kamu bisa langsung membuat password baru. +
+ + {{-- Session Status --}} + @if (session('status')) +{{ session('status') }}
+© 2026 FiFreshChecker. Tugas Akhir D3 Manajemen Informatika.
++ Masuk untuk menyimpan & melihat riwayat klasifikasi ikan kamu. +
++ Bergabunglah untuk mulai menganalisis kesegaran ikan dengan AI. +
++ Untuk akun {{ session('reset_email') }} +
+ + +© 2026 FiFreshChecker. Tugas Akhir D3 Manajemen Informatika.
++ Masukkan password baru kamu di bawah ini. +
+ + +© 2026 FiFreshChecker. Tugas Akhir D3 Manajemen Informatika.
++ Siap untuk mengecek kualitas ikan hari ini? Sistem klasifikasi AI kami siap memberikan hasil analisis yang akurat dalam hitungan detik. +
++Unggah citra mata ikan dan biarkan algoritma mendeteksi tingkat kesegarannya secara otomatis. +
+ + +Buka Fitur + + + ++Tinjau kembali data dan hasil pengecekan ikan yang telah Anda lakukan sebelumnya. +
+ + +Lihat Riwayat + + + ++Perbarui informasi akun dan pengaturan profil Anda. +
+ + +Kelola Akun + + + +Daftar hasil klasifikasi insang ikan yang pernah kamu lakukan.
+| No | +Gambar | +Tingkat Kesegaran | +Skor Tingkat Kesegaran | +Tanggal | +
|---|---|---|---|---|
| + #{{ method_exists($histories, 'currentPage') ? ($histories->currentPage() - 1) * $histories->perPage() + $loop->iteration : $loop->iteration }} + | +
+ |
+ + @if($history->classification->kelas == 'fresh') + + + {{ $history->classification->tingkat_kesegaran }} + + @else + + + {{ $history->classification->tingkat_kesegaran }} + + @endif + | +
+
+
+
+
+
+ {{ number_format($history->classification->prob_fresh * 100, 1) }}%
+
+
+
+ |
+
+
+ {{ $history->created_at->timezone('Asia/Jakarta')->format('d M Y') }}
+
+
+ {{ $history->created_at->timezone('Asia/Jakarta')->format('H:i') }} WIB
+
+ |
+
|
+
+
+
+ Belum ada riwayat analisis +Mulai analisis ikan pertamamu sekarang! + + Mulai Analisis + + |
+ ||||
+ Unggah foto insang ikan dengan pencahayaan yang baik, dan biarkan teknologi AI kami menganalisis kualitasnya secara real-time. +
+Insang jelas, pencahayaan baik
+Gelap, buram, bukan insang
+Status Kesegaran
+ @if(strtolower(session('kelas')) == 'segar' || strtolower(session('kelas')) == 'fresh') ++ Catatan Visual +
+ @if(strtolower(session('kelas')) == 'segar' || strtolower(session('kelas')) == 'fresh') ++ Berdasarkan analisis visual, fitur pada insang ikan ini menunjukkan ciri-ciri dalam kondisi baik. +
+ @else ++ Berdasarkan analisis visual, terdeteksi ciri-ciri penurunan kualitas pada insang ikan (seperti warna yang cenderung memucat atau menggelap). +
+ @endif +Skor Tingkat Kesegaran
++ {{ is_float(session('prob_fresh')) && session('prob_fresh') <= 1 ? number_format(session('prob_fresh') * 100, 1) : number_format(session('prob_fresh'), 1) }}% +
++ {{ session('error_insang') }} +
++ Saran +
++ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} +
++ {{ __('Ensure your account is using a long, random password to stay secure.') }} +
++ Kelola informasi data diri Anda. Pastikan nama lengkap sesuai. +
++ Teknologi Machine Learning dengan algoritma Naïve Bayes untuk menganalisis tingkat kesegaran ikan secara otomatis melalui citra insang ikan. +
++ FiFreshChecker adalah sistem klasifikasi kesegaran ikan berbasis Machine Learning yang menggunakan algoritma Naïve Bayes untuk menganalisis citra insang ikan secara otomatis. +
++ Sistem mengekstraksi fitur citra — mulai dari fitur warna LAB, HSV, color ratio, hingga tekstur GLCM — dan mengklasifikasikannya ke dalam 4 tingkat kesegaran: Sangat Segar, Segar, Cukup Segar, dan Tidak Segar. +
+Insang adalah jaringan yang paling cepat menunjukkan perubahan warna saat ikan mulai tidak segar — merah cerah menandakan kesegaran.
+Cukup buka tutup insang ikan dan foto close-up — tidak perlu alat khusus, cukup kamera smartphone.
+Warna, tekstur, dan pola insang mengandung informasi yang kaya untuk diekstraksi dan diklasifikasikan oleh model.
+Hanya 3 langkah sederhana dari foto hingga hasil klasifikasi
+Foto insang ikan dari jarak dekat menggunakan kamera smartphone. Pastikan insang terlihat jelas dan pencahayaan cukup.
+Sistem memvalidasi objek, mengekstraksi fitur citra warna dan tekstur, lalu diklasifikasikan oleh model Naïve Bayes.
+Dapatkan hasil klasifikasi 4 tingkat kesegaran beserta probabilitas dan visualisasi area insang yang dianalisis.
+Sistem yang dirancang untuk kemudahan penggunaan dengan analisis yang akurat dan terpercaya
+Unggah foto insang ikan langsung dari galeri atau ambil gambar menggunakan kamera smartphone. Format JPG, JPEG, dan PNG didukung.
+Model Naïve Bayes dilatih dengan dataset insang ikan untuk mengklasifikasikan tingkat kesegaran secara otomatis dan efisien.
+Tidak perlu akun atau login. Hasil klasifikasi beserta visualisasi area insang yang dianalisis tersedia dalam hitungan detik.
+Masuk atau daftar gratis untuk menyimpan semua hasil analisis ikan secara otomatis dan melihat riwayat klasifikasi kapan saja.
+Gratis · Tanpa Pendaftaran · Hasil dalam Detik
+