This commit is contained in:
destudenislam 2026-06-20 19:59:30 +07:00
parent 718760931d
commit 0c4ac2449d
27 changed files with 1626 additions and 148 deletions

View File

@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function showLogin() {
return view('auth.login');
}
public function login(Request $request) {
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials, $request->remember)) {
$request->session()->regenerate();
return redirect()->route('landingpage');
}
return back()->withErrors([
'email' => 'Email atau password yang Anda masukkan salah.',
])->onlyInput('email');
}
public function showRegister() {
return view('auth.register');
}
public function register(Request $request) {
$request->validate([
'name' => 'required|string|max:255',
// unique:users memastikan email belum terdaftar
'email' => 'required|string|email|max:255|unique:users',
// ditambahkan 'confirmed' agar wajib cocok dengan password_confirmation
'password' => 'required|string|min:8|confirmed',
], [
// Kostumisasi pesan error bahasa Indonesia
'email.unique' => 'Alamat email ini sudah terdaftar.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
'password.min' => 'Password minimal harus 8 karakter.',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
Auth::login($user);
return redirect()->route('landingpage');
}
public function logout(Request $request) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@ -0,0 +1,130 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Classification;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class ClassificationController extends Controller
{
public function process(Request $request)
{
// 🔹 Ambil base64 dari frontend
$imageData = $request->input('image');
$image = str_replace('data:image/png;base64,', '', $imageData);
$image = str_replace(' ', '+', $image);
// 🔹 Simpan sementara
$imageName = 'temp_' . time() . '.png';
Storage::disk('public')->put('scans/' . $imageName, base64_decode($image));
// 🔹 Path file untuk Python
$imagePath = storage_path('app/public/scans/' . $imageName);
// 🔹 Path Python & script
$python = "C:\\Program Files\\Python310\\python.exe";
$script = base_path('python/predict.py');
// 🔹 Jalankan Python
$command = "\"$python\" \"$script\" \"$imagePath\"";
$output = shell_exec($command);
// 🔥 HANDLE ERROR DARI PYTHON
if (!$output || str_contains($output, 'ERROR')) {
return response()->json([
'status' => 'error',
'message' => 'Gagal memproses gambar (objek tidak terdeteksi)'
], 500);
}
// 🔹 Format output: label|confidence
$result = explode('|', trim($output));
if (count($result) < 2) {
return response()->json([
'status' => 'error',
'message' => 'Format output Python tidak valid'
], 500);
}
$label = $result[0];
$confidence = round($result[1] * 100, 2);
// 🔹 Mapping label biar lebih user-friendly
$labelMap = [
'light' => 'Light Roast',
'medium' => 'Medium Roast',
'dark' => 'Dark Roast'
];
$finalLabel = $labelMap[$label] ?? $label;
return response()->json([
'status' => 'success',
'label' => $finalLabel,
'confidence' => $confidence . '%',
'image_name' => $imageName,
'image_url' => asset('storage/scans/' . $imageName),
'created_at' => now()->translatedFormat('d F Y, H:i') . ' WIB'
]);
}
public function store(Request $request)
{
Classification::create([
'user_id' => Auth::id(),
'image_path' => 'scans/' . $request->image_name,
'label' => $request->label,
'confidence' => str_replace('%', '', $request->confidence),
'color_data' => json_encode([
'r' => 120,
'g' => 80,
'b' => 50
])
]);
return response()->json(['message' => 'Data berhasil disimpan ke riwayat!']);
}
public function destroyTemp(Request $request)
{
$imagePath = 'scans/' . $request->image_name;
if (Storage::disk('public')->exists($imagePath)) {
Storage::disk('public')->delete($imagePath);
return response()->json(['message' => 'File sampah berhasil dibuang!']);
}
return response()->json(['message' => 'File tidak ditemukan'], 404);
}
public function history()
{
$histories = Classification::where('user_id', Auth::id())
->orderBy('created_at', 'desc')
->get();
return view('history', compact('histories'));
}
public function destroy($id)
{
$data = Classification::where('id', $id)
->where('user_id', Auth::id())
->firstOrFail();
if (Storage::disk('public')->exists($data->image_path)) {
Storage::disk('public')->delete($data->image_path);
}
$data->delete();
return redirect()->route('history')->with('success', 'Data riwayat berhasil dihapus.');
}
public function karakteristik()
{
return view('karakteristik');
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Classification extends Model
{
use HasFactory;
// Tambahkan baris ini agar data bisa disimpan ke database
protected $fillable = [
'user_id',
'image_path',
'label',
'confidence',
'color_data'
];
}

View File

@ -3,22 +3,19 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Auth\Notifications\ResetPassword;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
/**
* Register any application services.
*/
public function register(): void public function register(): void
{ {
// //
} }
/**
* Bootstrap any application services.
*/
public function boot(): void public function boot(): void
{ {
// ResetPassword::createUrlUsing(function ($user, string $token) {
return config('app.url') . '/reset-password/' . $token . '?email=' . urlencode($user->email);
});
} }
} }

View File

@ -70,7 +70,7 @@
| |
*/ */
'timezone' => 'UTC', 'timezone' => 'Asia/Jakarta',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('classifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('image_path');
$table->string('label');
$table->decimal('confidence', 5, 2);
$table->json('color_data')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('classifications');
}
};

BIN
public/img/contoh.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/img/dark-roast.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

BIN
public/img/light-roast.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

BIN
public/img/medium-roast.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
python/model_svm_kopi.pkl Normal file

Binary file not shown.

42
python/predict.py Normal file
View File

@ -0,0 +1,42 @@
import cv2
import numpy as np
import joblib
import os
import sys
image_path = sys.argv[1]
if not os.path.exists(image_path):
print("ERROR: file tidak ditemukan")
sys.exit()
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
model = joblib.load(os.path.join(BASE_DIR, 'model_svm_kopi.pkl'))
scaler = joblib.load(os.path.join(BASE_DIR, 'scaler_kopi.pkl'))
def extract_features(img):
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (224, 224))
mean = np.mean(img, axis=(0,1))
std = np.std(img, axis=(0,1))
return list(mean) + list(std)
img = cv2.imread(image_path)
if img is None:
print("ERROR")
sys.exit()
fitur = extract_features(img)
if fitur is None:
print("ERROR")
sys.exit()
fitur_scaled = scaler.transform([fitur])
pred = model.predict(fitur_scaled)[0]
prob = model.predict_proba(fitur_scaled).max()
print(f"{pred}|{prob}")

BIN
python/scaler_kopi.pkl Normal file

Binary file not shown.

View File

@ -0,0 +1,57 @@
@extends('layouts.app')
@section('content')
<div class="min-h-[85vh] flex items-center justify-center px-6 bg-white py-12">
<div class="max-w-md w-full">
<div class="bg-white rounded-[3rem] border border-gray-100 shadow-2xl overflow-hidden">
<div class="bg-gray-900 p-10 text-center">
<h2 class="text-3xl font-black text-white italic tracking-tighter">Lupa Password</h2>
<p class="text-gray-400 text-[10px] mt-2 uppercase tracking-[0.3em] font-bold">Masukkan email dan password baru</p>
</div>
<form action="{{ route('password.update.direct') }}" method="POST" class="p-10 space-y-5">
@csrf
@if(session('status'))
<div class="p-4 bg-green-50 text-green-700 rounded-2xl text-sm font-medium">
{{ session('status') }}
</div>
@endif
@if($errors->any())
<div class="p-4 bg-red-50 text-red-700 rounded-2xl text-sm font-medium">
{{ $errors->first() }}
</div>
@endif
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Email</label>
<input type="email" name="email" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all text-sm font-medium"
placeholder="email@anda.com">
</div>
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Password Baru</label>
<input type="password" name="password" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all text-sm font-medium"
placeholder="••••••••">
</div>
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Konfirmasi Password</label>
<input type="password" name="password_confirmation" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all text-sm font-medium"
placeholder="••••••••">
</div>
<button type="submit" class="w-full py-4 bg-orange-700 text-white rounded-2xl font-bold shadow-xl hover:bg-orange-800 transition-all">
Ubah Password
</button>
<p class="text-center text-xs text-gray-400 font-semibold">
<a href="{{ route('login') }}" class="text-orange-700 font-bold hover:underline">Kembali ke Login</a>
</p>
</form>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,81 @@
@extends('layouts.app')
@section('content')
<div class="min-h-[85vh] flex items-center justify-center px-6 bg-white relative overflow-hidden">
<div class="absolute top-0 right-0 w-96 h-96 bg-orange-50 rounded-full blur-3xl opacity-50 -translate-y-1/2 translate-x-1/2"></div>
<div class="absolute bottom-0 left-0 w-64 h-64 bg-gray-50 rounded-full blur-3xl opacity-50 translate-y-1/2 -translate-x-1/2"></div>
<div class="max-w-md w-full relative z-10">
<div class="bg-white rounded-[3rem] border border-gray-100 shadow-2xl overflow-hidden transition-all duration-500">
<div class="bg-gray-900 p-12 text-center relative">
<div class="relative z-10">
<div class="w-16 h-16 bg-orange-700 rounded-2xl mx-auto mb-6 flex items-center justify-center rotate-3 shadow-lg shadow-orange-700/40">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.799-2.753 9.571m-1.14-10.49c1.458-1.46 3.542-1.46 5 0m-8.544 1.15c-1.458 1.46-3.542 1.46-5 0m1.14 10.49C1.009 17.799 0 14.517 0 11c0-3.517 1.009-6.799 2.753-9.571m1.14 10.49c1.458-1.46 3.542-1.46 5 0m8.544-1.15c1.458-1.46 3.542-1.46 5 0m-1.14-10.49C22.991 4.201 24 7.483 24 11c0 3.517-1.009 6.799-2.753 9.571" />
</svg>
</div>
<h2 class="text-3xl font-black text-white italic tracking-tighter">RobustaMeter<span class="text-orange-600">.id</span></h2>
<p class="text-gray-400 text-xs mt-2 uppercase tracking-[0.3em] font-bold">Akses Dashboard Digital</p>
</div>
<div class="absolute top-0 right-0 w-32 h-32 bg-orange-600/10 rounded-full blur-3xl"></div>
</div>
<form action="{{ route('login') }}" method="POST" class="p-10 space-y-6">
@csrf
<div class="space-y-2">
<label for="email" class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Alamat Email</label>
<div class="relative group">
<input type="email" id="email" name="email" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all placeholder:text-gray-300 text-sm font-medium @error('email') border-red-500 @enderror"
placeholder="nama@email.com" value="{{ old('email') }}">
@error('email')
<p class="text-[10px] text-red-500 mt-1 ml-1 font-bold italic">{{ $message }}</p>
@enderror
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between items-center ml-1">
<label for="password" class="text-[10px] font-black uppercase text-gray-400 tracking-widest">Password</label>
<a href="{{ route('password.request') }}" class="text-[10px] font-bold text-orange-700 hover:text-orange-800 transition">Lupa Kata Sandi?</a>
</div>
<div class="relative group border-none">
<input type="password" id="password" name="password" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all placeholder:text-gray-300 text-sm font-medium"
placeholder="••••••••">
</div>
</div>
<div class="flex items-center justify-between px-1">
<label class="flex items-center gap-2 cursor-pointer group">
<input type="checkbox" name="remember" class="w-4 h-4 rounded border-gray-300 text-orange-700 focus:ring-orange-700 accent-orange-700">
<span class="text-xs text-gray-500 font-semibold group-hover:text-gray-700 transition">Ingat saya</span>
</label>
</div>
<button type="submit" class="w-full py-4 bg-orange-700 text-white rounded-2xl font-bold shadow-xl shadow-orange-700/30 hover:bg-orange-800 hover:-translate-y-1 active:scale-95 transition-all duration-300 flex items-center justify-center gap-2 group">
<span>Masuk ke Akun</span>
<svg class="w-4 h-4 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</button>
<div class="text-center pt-4">
<p class="text-xs text-gray-400 font-semibold">Belum memiliki akun?
<a href="{{ route('register') }}" class="text-orange-700 font-bold hover:underline underline-offset-4 transition">Daftar Sekarang</a>
</p>
</div>
</form>
</div>
<div class="text-center mt-8">
<a href="/" class="text-xs font-bold text-gray-400 hover:text-gray-600 transition flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
Kembali ke Beranda
</a>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,67 @@
@extends('layouts.app')
@section('content')
<div class="min-h-[85vh] flex items-center justify-center px-6 bg-white relative overflow-hidden py-12">
<div class="absolute top-0 left-0 w-96 h-96 bg-orange-50 rounded-full blur-3xl opacity-50 -translate-y-1/2 -translate-x-1/2"></div>
<div class="max-w-md w-full relative z-10">
<div class="bg-white rounded-[3rem] border border-gray-100 shadow-2xl overflow-hidden">
<div class="bg-gray-900 p-10 text-center relative">
<h2 class="text-3xl font-black text-white italic tracking-tighter">Gabung Sekarang</h2>
<p class="text-gray-400 text-[10px] mt-2 uppercase tracking-[0.3em] font-bold">Mulai Perjalanan Kopi Anda</p>
</div>
<form action="{{ route('register') }}" method="POST" class="p-10 space-y-5">
@csrf
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Nama Lengkap</label>
<input type="text" name="name" value="{{ old('name') }}" required
class="w-full px-6 py-4 bg-gray-50 border @error('name') border-red-500 @else border-gray-100 @enderror rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all text-sm font-medium"
placeholder="Masukkan nama Anda">
@error('name')
<span class="text-xs text-red-500 ml-1 block mt-1">{{ $message }}</span>
@enderror
</div>
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Alamat Email</label>
<input type="email" name="email" value="{{ old('email') }}" required
class="w-full px-6 py-4 bg-gray-50 border @error('email') border-red-500 @else border-gray-100 @enderror rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all text-sm font-medium"
placeholder="email@anda.com">
@error('email')
<span class="text-xs text-red-500 ml-1 block mt-1">{{ $message }}</span>
@enderror
</div>
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Password</label>
<input type="password" name="password" required
class="w-full px-6 py-4 bg-gray-50 border @error('password') border-red-500 @else border-gray-100 @enderror rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all text-sm font-medium"
placeholder="••••••••">
@error('password')
<span class="text-xs text-red-500 ml-1 block mt-1">{{ $message }}</span>
@enderror
</div>
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Konfirmasi Password</label>
<input type="password" name="password_confirmation" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-4 focus:ring-orange-700/5 focus:border-orange-700 outline-none transition-all text-sm font-medium"
placeholder="••••••••">
</div>
<button type="submit" class="w-full py-4 bg-orange-700 text-white rounded-2xl font-bold shadow-xl shadow-orange-700/30 hover:bg-orange-800 hover:-translate-y-1 active:scale-95 transition-all duration-300 mt-4">
Buat Akun Gratis
</button>
<p class="text-center text-xs text-gray-400 font-semibold pt-2">
Sudah punya akun?
<a href="{{ route('login') }}" class="text-orange-700 font-bold hover:underline">Masuk di sini</a>
</p>
</form>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,38 @@
@extends('layouts.app')
@section('content')
<div class="min-h-[85vh] flex items-center justify-center px-6 bg-white py-12">
<div class="max-w-md w-full">
<div class="bg-white rounded-[3rem] border border-gray-100 shadow-2xl overflow-hidden">
<div class="bg-gray-900 p-10 text-center">
<h2 class="text-3xl font-black text-white italic tracking-tighter">Reset Password</h2>
<p class="text-gray-400 text-[10px] mt-2 uppercase tracking-[0.3em] font-bold">Masukkan password baru</p>
</div>
<form action="{{ route('password.update') }}" method="POST" class="p-10 space-y-5">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Email</label>
<input type="email" name="email" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl outline-none text-sm font-medium"
placeholder="email@anda.com">
</div>
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Password Baru</label>
<input type="password" name="password" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl outline-none text-sm font-medium"
placeholder="••••••••">
</div>
<div class="space-y-1">
<label class="text-[10px] font-black uppercase text-gray-400 ml-1 tracking-widest">Konfirmasi Password</label>
<input type="password" name="password_confirmation" required
class="w-full px-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl outline-none text-sm font-medium"
placeholder="••••••••">
</div>
<button type="submit" class="w-full py-4 bg-orange-700 text-white rounded-2xl font-bold shadow-xl hover:bg-orange-800 transition-all">
Reset Password
</button>
</form>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,49 @@
@extends('layouts.app')
@section('content')
<div class="max-w-6xl mx-auto py-16 px-6">
<div class="text-center mb-16">
<h1 class="text-4xl font-black text-gray-900 tracking-tight">Halo, <span class="text-orange-700">{{ Auth::user()->name }}!</span></h1>
<p class="text-gray-500 mt-2">Siap untuk melakukan klasifikasi biji kopi hari ini?</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
<div class="group p-8 bg-white border border-gray-100 rounded-[2.5rem] shadow-sm hover:shadow-xl transition-all duration-500 text-center">
<div class="w-16 h-16 bg-orange-50 text-orange-700 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:rotate-6 transition">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path></svg>
</div>
<h4 class="font-bold text-lg mb-2">1. Persiapkan Sampel</h4>
<p class="text-gray-500 text-sm leading-relaxed">Letakkan satu biji kopi Robusta di atas alas putih polos dengan cahaya yang terang.</p>
</div>
<div class="group p-8 bg-orange-700 rounded-[2.5rem] shadow-xl text-center text-white relative overflow-hidden">
<div class="w-16 h-16 bg-white/20 text-white rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<h4 class="font-bold text-lg mb-2">2. Jalankan Analisis</h4>
<p class="text-orange-100 text-sm leading-relaxed text-balance">Buka kamera dan ambil gambar. Sistem akan melakukan ekstraksi fitur warna secara instan.</p>
<div class="absolute -right-4 -bottom-4 w-24 h-24 bg-white/10 rounded-full blur-2xl"></div>
</div>
<div class="group p-8 bg-white border border-gray-100 rounded-[2.5rem] shadow-sm hover:shadow-xl transition-all duration-500 text-center">
<div class="w-16 h-16 bg-orange-50 text-orange-700 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:-rotate-6 transition">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</div>
<h4 class="font-bold text-lg mb-2">3. Lihat Hasil SVM</h4>
<p class="text-gray-500 text-sm leading-relaxed">Dapatkan hasil klasifikasi tingkat kematangan beserta skor akurasi model.</p>
</div>
</div>
<div class="flex flex-col md:flex-row items-center justify-center gap-6">
<a href="{{ route('klasifikasi') }}" class="w-full md:w-auto inline-flex items-center justify-center gap-4 px-12 py-5 bg-gray-900 text-white rounded-3xl font-bold shadow-2xl hover:bg-black hover:-translate-y-2 transition-all duration-300 group">
<span>Buka Kamera Klasifikasi</span>
<svg class="w-5 h-5 group-hover:translate-x-2 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path></svg>
</a>
<a href="{{ route('history') }}" class="w-full md:w-auto inline-flex items-center justify-center gap-4 px-10 py-5 bg-white border border-gray-200 text-gray-900 rounded-3xl font-bold hover:bg-gray-50 hover:-translate-y-1 transition-all duration-300 shadow-sm">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>Lihat Riwayat Data</span>
</a>
</div>
</div>
@endsection

View File

@ -0,0 +1,139 @@
@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto py-12 px-6">
<div class="flex flex-col md:flex-row justify-between items-center mb-10 gap-4 border-b pb-8">
<div class="flex items-center gap-5">
<a href="{{ url()->previous() }}" class="w-10 h-10 bg-white border border-gray-100 rounded-xl flex items-center justify-center shadow-sm hover:bg-orange-50 hover:border-orange-200 transition-all group" title="Kembali ke Dashboard">
<svg class="w-5 h-5 text-gray-400 group-hover:text-orange-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</a>
<div>
<h1 class="text-4xl font-black text-gray-900 tracking-tight leading-none uppercase">RIWAYAT <span class="text-orange-700">KLASIFIKASI</span></h1>
<p class="text-gray-500 mt-1 uppercase text-[10px] font-bold tracking-[0.2em]">Daftar hasil analisis biji kopi yang tersimpan</p>
</div>
</div>
<div class="hidden md:block text-right">
<span class="text-[10px] font-black uppercase text-gray-300 tracking-[0.3em]">Total Data: {{ count($histories) }} Sampel</span>
</div>
</div>
<div class="bg-white border rounded-[2.5rem] overflow-hidden shadow-sm">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 border-b">
<th class="px-8 py-5 text-[10px] font-black uppercase tracking-widest text-gray-400">Sampel</th>
<th class="px-6 py-5 text-[10px] font-black uppercase tracking-widest text-gray-400 text-center">Hasil Prediksi</th>
<th class="px-6 py-5 text-[10px] font-black uppercase tracking-widest text-gray-400 text-center">Akurasi</th>
<th class="px-6 py-5 text-[10px] font-black uppercase tracking-widest text-gray-400 text-center">Tanggal & Waktu</th>
<th class="px-8 py-5 text-[10px] font-black uppercase tracking-widest text-gray-400 text-right">Aksi</th>
</tr>
</thead>
<tbody class="divide-y">
@forelse($histories as $data)
<tr class="hover:bg-gray-50/50 transition-colors group">
<td class="px-8 py-4">
<div class="w-16 h-16 rounded-2xl overflow-hidden border-2 border-white shadow-sm ring-1 ring-gray-100">
<img src="{{ asset('storage/' . $data->image_path) }}" class="w-full h-full object-cover" alt="Sampel Biji Kopi">
</div>
</td>
<td class="px-6 py-4 text-center">
<span class="px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-wider
{{ $data->label == 'Dark Roast' ? 'bg-gray-900 text-white' : ($data->label == 'Medium Roast' ? 'bg-orange-100 text-orange-700' : 'bg-orange-50 text-orange-600') }}">
{{ $data->label }}
</span>
</td>
<td class="px-6 py-4 text-center font-bold text-gray-900 text-sm italic">
{{ $data->confidence }}%
</td>
<td class="px-6 py-4 text-center">
<p class="text-[11px] font-bold text-gray-600 uppercase">{{ \Carbon\Carbon::parse($data->created_at)->translatedFormat('d F Y') }}</p>
<p class="text-[10px] text-gray-400 font-bold uppercase tracking-tighter">{{ \Carbon\Carbon::parse($data->created_at)->format('H:i') }} WIB</p>
</td>
<td class="px-8 py-4 text-right">
<button onclick="openDeleteModal('{{ $data->id }}')" class="text-[10px] font-bold text-gray-300 hover:text-red-600 transition uppercase tracking-widest">
Hapus
</button>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-8 py-20 text-center">
<p class="text-gray-400 font-medium italic">Belum ada data riwayat yang disimpan.</p>
<a href="{{ route('klasifikasi') }}" class="text-orange-700 font-bold text-xs hover:underline mt-2 inline-block">Mulai Klasifikasi Sekarang</a>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div id="deleteModal" class="fixed inset-0 z-[60] hidden overflow-y-auto">
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity"></div>
<div class="flex min-h-full items-center justify-center p-4 text-center">
<div class="relative transform overflow-hidden rounded-[2.5rem] bg-white p-8 text-left shadow-2xl transition-all w-full max-w-md border border-gray-100 animate-fadeIn">
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-red-50 mb-6">
<svg class="h-10 w-10 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</div>
<div class="text-center">
<h3 class="text-xl font-black text-gray-900 uppercase tracking-tight mb-2">Hapus Riwayat?</h3>
<p class="text-sm text-gray-500 leading-relaxed mb-8 text-balance">Data klasifikasi dan foto sampel ini akan dihapus permanen dari sistem. Tindakan ini tidak dapat dibatalkan.</p>
</div>
<div class="flex flex-col gap-3">
<form id="deleteForm" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="w-full py-4 bg-red-600 text-white rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] shadow-lg shadow-red-200 hover:bg-red-700 transition-all">
Ya, Hapus
</button>
</form>
<button onclick="closeDeleteModal()" class="w-full py-4 bg-gray-100 text-gray-500 rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] border border-gray-200 hover:bg-gray-200 transition-all">
Batalkan
</button>
</div>
</div>
</div>
</div>
<style>
.animate-fadeIn { animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
</style>
@endsection
@push('scripts')
<script>
const modal = document.getElementById('deleteModal');
const form = document.getElementById('deleteForm');
function openDeleteModal(id) {
form.action = `/classification/${id}`;
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function closeDeleteModal() {
modal.classList.add('hidden');
document.body.style.overflow = 'auto';
}
window.onclick = function(event) {
if (event.target == modal) {
closeDeleteModal();
}
}
</script>
@endpush

View File

@ -0,0 +1,125 @@
@extends('layouts.app')
@section('content')
<section class="relative pt-16 pb-16 bg-white overflow-hidden border-b border-gray-100">
<div class="absolute top-0 left-0 w-96 h-96 bg-orange-50 rounded-full blur-3xl opacity-50 -translate-x-1/2 -translate-y-1/2"></div>
<div class="container mx-auto px-10 relative z-10 text-center max-w-3xl">
<span class="px-4 py-2 bg-orange-50 text-orange-700 text-sm font-bold rounded-full tracking-widest uppercase mb-4 inline-block">Edukasi Kopi</span>
<h1 class="text-5xl font-extrabold text-gray-900 leading-tight mb-4">
Karakteristik <span class="text-transparent bg-clip-text bg-gradient-to-r from-orange-700 to-amber-500">Tingkat Roasting</span>
</h1>
<p class="text-gray-500 text-lg leading-relaxed">
Kenali perbedaan warna, profil rasa, dan karakteristik kafein dari setiap tingkatan kematangan biji kopi robusta untuk menemukan preferensi seduhan terbaik Anda.
</p>
</div>
</section>
<section class="py-20 bg-gray-50">
<div class="container mx-auto px-10 max-w-6xl">
<div class="grid grid-cols-1 md:grid-cols-3 gap-10">
<div class="bg-white rounded-[2.5rem] p-8 shadow-sm border border-gray-100 hover:shadow-xl hover:border-orange-200 transition-all duration-300 group">
<div class="aspect-square bg-gray-100 rounded-3xl mb-8 overflow-hidden relative">
<img src="{{ asset('img/light-roast.jpg') }}" alt="Light Roast" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700">
<div class="absolute top-4 right-4 bg-white/90 backdrop-blur px-3 py-1 rounded-full text-xs font-bold text-gray-900">180°C - 205°C</div>
</div>
<h3 class="text-2xl font-black text-gray-900 mb-2">Light Roast</h3>
<p class="text-orange-700 font-bold text-sm uppercase tracking-widest mb-6">Sangrai Rendah</p>
<div class="space-y-4 text-sm">
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path></svg>
<p><span class="font-bold text-gray-900">Warna Fisik:</span> Cokelat muda cerah, permukaan sangat kering tanpa kilau minyak.</p>
</div>
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p><span class="font-bold text-gray-900">Profil Rasa:</span> Keasaman (acidity) tinggi, cita rasa khas daerah tanam (fruity/herbal) sangat menonjol.</p>
</div>
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
<p><span class="font-bold text-gray-900">Kadar Kafein:</span> Sangat tinggi.</p>
</div>
<div class="p-4 bg-orange-50 rounded-2xl mt-4">
<span class="block text-xs font-bold text-orange-700 uppercase mb-1">Rekomendasi Seduh</span>
<p class="text-gray-700 font-medium">Manual Brew (V60, Chemex, AeroPress)</p>
</div>
</div>
</div>
<div class="bg-white rounded-[2.5rem] p-8 shadow-sm border border-gray-100 hover:shadow-xl hover:border-orange-200 transition-all duration-300 group">
<div class="aspect-square bg-gray-100 rounded-3xl mb-8 overflow-hidden relative">
<img src="{{ asset('img/medium-roast.jpg') }}" alt="Medium Roast" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700">
<div class="absolute top-4 right-4 bg-white/90 backdrop-blur px-3 py-1 rounded-full text-xs font-bold text-gray-900">210°C - 220°C</div>
</div>
<h3 class="text-2xl font-black text-gray-900 mb-2">Medium Roast</h3>
<p class="text-orange-700 font-bold text-sm uppercase tracking-widest mb-6">Sangrai Sedang</p>
<div class="space-y-4 text-sm">
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path></svg>
<p><span class="font-bold text-gray-900">Warna Fisik:</span> Cokelat sedang (seperti cokelat batangan), permukaan mulai terlihat sedikit berminyak.</p>
</div>
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p><span class="font-bold text-gray-900">Profil Rasa:</span> Sangat seimbang (balanced). Keasaman berkurang, muncul aroma manis seperti karamel atau kacang (nutty).</p>
</div>
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
<p><span class="font-bold text-gray-900">Kadar Kafein:</span> Sedang.</p>
</div>
<div class="p-4 bg-orange-50 rounded-2xl mt-4">
<span class="block text-xs font-bold text-orange-700 uppercase mb-1">Rekomendasi Seduh</span>
<p class="text-gray-700 font-medium">Americano, Long Black, Syphon</p>
</div>
</div>
</div>
<div class="bg-white rounded-[2.5rem] p-8 shadow-sm border border-gray-100 hover:shadow-xl hover:border-orange-200 transition-all duration-300 group">
<div class="aspect-square bg-gray-100 rounded-3xl mb-8 overflow-hidden relative">
<img src="{{ asset('img/dark-roast.jpg') }}" alt="Dark Roast" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700">
<div class="absolute top-4 right-4 bg-white/90 backdrop-blur px-3 py-1 rounded-full text-xs font-bold text-gray-900">225°C - 240°C</div>
</div>
<h3 class="text-2xl font-black text-gray-900 mb-2">Dark Roast</h3>
<p class="text-orange-700 font-bold text-sm uppercase tracking-widest mb-6">Sangrai Tinggi</p>
<div class="space-y-4 text-sm">
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path></svg>
<p><span class="font-bold text-gray-900">Warna Fisik:</span> Cokelat sangat tua hingga kehitaman, permukaan mengkilap dan sangat berminyak (oily).</p>
</div>
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p><span class="font-bold text-gray-900">Profil Rasa:</span> Rasa pahit dominan, body sangat tebal. Aroma panggangan (smoky, dark chocolate) sangat pekat.</p>
</div>
<div class="flex gap-3">
<svg class="w-5 h-5 text-orange-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
<p><span class="font-bold text-gray-900">Kadar Kafein:</span> Paling rendah.</p>
</div>
<div class="p-4 bg-orange-50 rounded-2xl mt-4">
<span class="block text-xs font-bold text-orange-700 uppercase mb-1">Rekomendasi Seduh</span>
<p class="text-gray-700 font-medium">Espresso, Café Latte, Kopi Susu Aren</p>
</div>
</div>
</div>
</div>
<div class="mt-20 text-center">
@auth
<a href="{{ route('klasifikasi') }}" class="inline-flex items-center gap-3 px-10 py-4 bg-gray-900 text-white rounded-full font-bold hover:bg-black transition-all shadow-xl">
<span>Coba Klasifikasi Sekarang</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
</a>
@else
<a href="{{ route('login') }}" class="inline-flex items-center gap-3 px-10 py-4 bg-gray-900 text-white rounded-full font-bold hover:bg-black transition-all shadow-xl">
<span>Login untuk Klasifikasi</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path></svg>
</a>
@endauth
</div>
</div>
</section>
@endsection

View File

@ -0,0 +1,406 @@
@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto py-12 px-6">
<div class="flex flex-col md:flex-row justify-between items-center mb-10 gap-4 border-b pb-8">
<div class="flex items-center gap-4">
<a href="{{ url()->previous() }}" class="w-10 h-10 bg-white border border-gray-100 rounded-xl flex items-center justify-center shadow-sm hover:bg-orange-50 hover:border-orange-200 transition-all group" title="Kembali"> <svg class="w-5 h-5 text-gray-400 group-hover:text-orange-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</a>
<div>
<h1 class="text-4xl font-black text-gray-900 tracking-tight leading-none uppercase">CITRA <span class="text-orange-700">DIGITAL</span></h1>
<p class="text-gray-500 mt-1 uppercase text-[10px] font-bold tracking-[0.2em]">Sistem Klasifikasi SVM</p>
</div>
</div>
<div class="flex items-center gap-4">
<a href="{{ route('history') }}" class="flex items-center gap-2 px-5 py-2.5 bg-gray-900 text-white rounded-xl hover:bg-orange-700 transition-all shadow-lg active:scale-95 group">
<svg class="w-4 h-4 text-gray-400 group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-[10px] font-black uppercase tracking-widest">Lihat Riwayat</span>
</a>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
<div class="lg:col-span-3 space-y-4">
<div class="bg-white border rounded-[2rem] p-6 shadow-sm">
<h3 class="font-bold text-sm mb-4 uppercase tracking-wider text-gray-400">Prosedur</h3>
<ul class="space-y-4">
<li class="flex gap-3">
<div class="w-6 h-6 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-xs font-bold flex-none">1</div>
<p class="text-xs text-gray-600 leading-relaxed">Letakkan satu biji kopi di atas permukaan <strong>putih polos</strong>.</p>
</li>
<li class="flex gap-3">
<div class="w-6 h-6 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-xs font-bold flex-none">2</div>
<p class="text-xs text-gray-600 leading-relaxed">Pastikan biji berada tepat di dalam <strong>kotak bidik</strong>.</p>
</li>
<li class="flex gap-3">
<div class="w-6 h-6 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-xs font-bold flex-none">3</div>
<p class="text-xs text-gray-600 leading-relaxed">Klik tombol klasifikasi dan tunggu <strong>proses SVM</strong> selesai.</p>
</li>
</ul>
</div>
<div class="bg-white border rounded-[2rem] p-6 shadow-sm">
<h3 class="font-bold text-sm mb-3 uppercase tracking-wider text-orange-700">Contoh Citra Ideal (Dataset)</h3>
<div class="space-y-3">
<div class="w-full aspect-square rounded-2xl overflow-hidden border border-gray-100 bg-gray-50 flex items-center justify-center p-2">
<img src="{{ asset('img/contoh.jpg') }}" alt="Contoh Dataset" class="w-full h-full object-contain rounded-xl">
</div>
<div class="bg-orange-50/70 rounded-xl p-3 border border-orange-100">
<p class="text-[11px] text-orange-800 leading-relaxed font-medium">
<strong>💡 Tips Karakteristik Webcam:</strong> Jika gambar dari webcam Anda terlihat agak redup/gelap, dekatkan lampu eksternal (seperti lampu belajar atau flash HP) ke arah alas putih agar distribusi warna piksel mendekati contoh gambar dataset di atas.
</p>
</div>
</div>
</div>
<div class="bg-gray-900 rounded-[2.5rem] p-6 text-white overflow-hidden relative group">
<div class="relative z-10 space-y-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
<p class="text-[10px] font-black uppercase tracking-[0.2em] text-orange-500">Standarisasi Digital</p>
</div>
<p class="text-sm font-bold leading-snug">Deteksi warna dilakukan secara objektif oleh sistem pintar.</p>
</div>
</div>
</div>
<div class="lg:col-span-6">
<div class="relative">
<div class="relative bg-black rounded-[3rem] overflow-hidden aspect-square border-[8px] border-white shadow-2xl ring-1 ring-gray-200">
<video id="video" class="w-full h-full object-cover" autoplay playsinline></video>
<div id="scanner-line" class="hidden absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-transparent via-orange-500 to-transparent shadow-[0_0_15px_rgba(249,115,22,0.8)] z-30 animate-scan"></div>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none z-20">
<div class="w-48 h-48 border border-white/30 rounded-3xl flex items-center justify-center relative">
<div class="absolute -top-1 -left-1 w-6 h-6 border-t-4 border-l-4 border-orange-500 rounded-tl-lg"></div>
<div class="absolute -top-1 -right-1 w-6 h-6 border-t-4 border-r-4 border-orange-500 rounded-tr-lg"></div>
<div class="absolute -bottom-1 -left-1 w-6 h-6 border-b-4 border-l-4 border-orange-500 rounded-bl-lg"></div>
<div class="absolute -bottom-1 -right-1 w-6 h-6 border-b-4 border-r-4 border-orange-500 rounded-br-lg"></div>
<div class="w-1 h-1 bg-orange-500 rounded-full shadow-[0_0_8px_#f97316]"></div>
</div>
</div>
</div>
<div id="capture-area" class="absolute -bottom-6 inset-x-0 flex flex-col items-center gap-3 z-40 transition-all duration-500">
<!-- Tombol Webcam -->
<button id="capture" class="group flex items-center gap-3 bg-white px-8 py-4 rounded-full shadow-xl border border-gray-100 hover:bg-orange-700 hover:text-white transition-all duration-300 active:scale-95">
<div class="w-10 h-10 bg-orange-700 group-hover:bg-white rounded-full flex items-center justify-center transition-colors">
<svg class="w-5 h-5 text-white group-hover:text-orange-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z">
</path>
</svg>
</div>
<span class="font-bold uppercase tracking-widest text-sm text-gray-900 group-hover:text-white">
Jalankan Klasifikasi
</span>
</button>
<!-- Tombol Upload -->
<label class="group flex items-center gap-2 bg-gray-100 px-6 py-3 rounded-full border border-gray-200 hover:bg-orange-700 hover:text-white transition-all duration-300 cursor-pointer shadow-md">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4-4m0 0l-4 4m4-4v12"/>
</svg>
<span class="font-bold uppercase tracking-widest text-[11px]">
Upload Gambar
</span>
<input type="file" id="upload-image" accept="image/*" class="hidden">
</label>
</div>
</div>
</div>
<div class="lg:col-span-3">
<div id="result-area" class="h-full">
<div class="bg-white rounded-[2.5rem] border p-8 flex flex-col h-full shadow-sm relative overflow-hidden transition-all duration-500">
<div id="placeholder" class="m-auto text-center space-y-4">
<div class="w-16 h-16 bg-gray-50 rounded-2xl flex items-center justify-center mx-auto border border-dashed text-gray-300">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg>
</div>
<p class="text-xs text-gray-400 font-medium tracking-wide">Menunggu input gambar...</p>
</div>
<div id="actual-result" class="hidden space-y-6 animate-fadeIn">
<div class="text-center">
<p class="text-[10px] font-black text-orange-700 uppercase tracking-[0.3em] mb-1">Hasil Prediksi</p>
<h2 id="prediction-text" class="text-4xl font-black text-gray-900 leading-none italic uppercase">MEDIUM</h2>
<div class="mt-3 flex items-center justify-center gap-2 text-gray-400">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p id="result-date" class="text-[9px] font-bold uppercase tracking-wider"></p>
</div>
</div>
<div class="space-y-4">
<div class="w-full aspect-square rounded-3xl overflow-hidden border-4 border-gray-50 shadow-inner bg-gray-100">
<canvas id="canvas" class="w-full h-full object-cover"></canvas>
</div>
<div class="bg-gray-50 rounded-2xl p-4 space-y-3 border border-gray-100">
<div class="flex justify-between text-[10px] font-black uppercase text-gray-400">
<span>Confidence Score</span>
<span class="text-gray-900" id="confidence">0%</span>
</div>
<div class="h-1.5 w-full bg-gray-200 rounded-full overflow-hidden">
<div id="confidence-bar" class="h-full bg-orange-600 transition-all duration-1000" style="width: 0%"></div>
</div>
</div>
</div>
<div id="action-area" class="space-y-3">
<button id="save-btn" class="w-full py-4 bg-orange-700 text-white rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] shadow-lg flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg>
Simpan ke Riwayat
</button>
<button id="reset-btn" class="w-full py-4 bg-gray-100 text-gray-500 hover:text-gray-700 rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] border border-gray-200">
Abaikan & Analisis Ulang
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
@keyframes scan { 0% { top: 0%; opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { top: 100%; opacity: 0; } }
.animate-scan { animation: scan 2s linear infinite; }
.animate-fadeIn { animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
</style>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const video = document.getElementById('video');
const captureArea = document.getElementById('capture-area');
const captureBtn = document.getElementById('capture');
const scannerLine = document.getElementById('scanner-line');
const placeholder = document.getElementById('placeholder');
const actualResult = document.getElementById('actual-result');
const canvas = document.getElementById('canvas');
const predictionText = document.getElementById('prediction-text');
const confidenceLabel = document.getElementById('confidence');
const confidenceBar = document.getElementById('confidence-bar');
const resultDate = document.getElementById('result-date');
const actionArea = document.getElementById('action-area');
let currentTempData = null;
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({
video: true
})
.then(function(stream) {
video.srcObject = stream;
video.play();
})
.catch(err => {
console.error("Kamera Error:", err);
alert("Kamera tidak ditemukan.");
});
}
function handleIgnore() {
if (currentTempData) {
fetch("{{ route('api.destroy.temp') }}", {
method: "POST",
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": "{{ csrf_token() }}" },
body: JSON.stringify({ image_name: currentTempData.image_name })
});
}
resetToScanMode();
}
function handleSave() {
const saveBtn = document.getElementById('save-btn');
saveBtn.disabled = true;
saveBtn.innerText = "Menyimpan...";
fetch("{{ route('api.save') }}", {
method: "POST",
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": "{{ csrf_token() }}" },
body: JSON.stringify({
image_name: currentTempData.image_name,
label: currentTempData.label,
confidence: currentTempData.confidence
})
})
.then(res => res.json())
.then(res => {
actionArea.innerHTML = `
<button id="scan-lagi-btn" class="w-full py-4 bg-green-600 text-white rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] hover:bg-green-700 shadow-lg animate-fadeIn flex items-center justify-center gap-2">
<span>Berhasil Tersimpan </span>
</button>
`;
document.getElementById('scan-lagi-btn').onclick = resetToScanMode;
});
}
function resetToScanMode() {
actualResult.classList.add('hidden');
placeholder.classList.remove('hidden');
captureArea.classList.remove('hidden', 'opacity-0');
currentTempData = null;
actionArea.innerHTML = `
<button id="save-btn" class="w-full py-4 bg-orange-700 text-white rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] shadow-lg flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg>
Simpan ke Riwayat
</button>
<button id="reset-btn" class="w-full py-4 bg-gray-100 text-gray-500 hover:text-gray-700 rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] border border-gray-200 mt-3">
Abaikan & Analisis Ulang
</button>
`;
captureBtn.disabled = false;
captureBtn.querySelector('span').innerText = "Jalankan Klasifikasi";
scannerLine.classList.add('hidden');
document.getElementById('save-btn').onclick = handleSave;
document.getElementById('reset-btn').onclick = handleIgnore;
}
const uploadInput = document.getElementById('upload-image');
uploadInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
scannerLine.classList.remove('hidden');
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
const context = canvas.getContext('2d');
// Samakan preprocessing dengan model
canvas.width = 224;
canvas.height = 224;
context.drawImage(img, 0, 0, 224, 224);
// Convert canvas ke base64
const finalImage = canvas.toDataURL('image/png');
fetch("{{ route('api.classify') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
image: finalImage
})
})
.then(res => res.json())
.then(data => {
scannerLine.classList.add('hidden');
if (data.status === 'error') {
alert(data.message);
return;
}
captureArea.classList.add('hidden', 'opacity-0');
placeholder.classList.add('hidden');
actualResult.classList.remove('hidden');
predictionText.innerText = data.label;
confidenceLabel.innerText = data.confidence;
confidenceBar.style.width = data.confidence;
resultDate.innerText = data.created_at;
currentTempData = data;
document.getElementById('save-btn').onclick = handleSave;
document.getElementById('reset-btn').onclick = handleIgnore;
})
.catch(err => {
console.log(err);
alert("Gagal upload gambar.");
scannerLine.classList.add('hidden');
});
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
captureBtn.addEventListener('click', function() {
scannerLine.classList.remove('hidden');
captureBtn.disabled = true;
captureBtn.querySelector('span').innerText = "Menganalisis...";
const context = canvas.getContext('2d');
// Ambil ukuran video
const vw = video.videoWidth;
const vh = video.videoHeight;
const cropSize = Math.min(vw, vh) * 0.6;
const startX = (vw - cropSize) / 2;
const startY = (vh - cropSize) / 2;
canvas.width = cropSize;
canvas.height = cropSize;
context.drawImage(video, startX, startY, cropSize, cropSize, 0, 0, cropSize, cropSize);
const imageData = canvas.toDataURL('image/png');
fetch("{{ route('api.classify') }}", {
method: "POST",
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": "{{ csrf_token() }}" },
body: JSON.stringify({ image: imageData })
})
.then(res => res.json())
.then(data => {
scannerLine.classList.add('hidden');
captureArea.classList.add('hidden', 'opacity-0');
placeholder.classList.add('hidden');
actualResult.classList.remove('hidden');
predictionText.innerText = data.label;
confidenceLabel.innerText = data.confidence;
confidenceBar.style.width = data.confidence;
resultDate.innerText = data.created_at;
currentTempData = data;
document.getElementById('save-btn').onclick = handleSave;
document.getElementById('reset-btn').onclick = handleIgnore;
})
.catch(err => {
alert("Gagal klasifikasi.");
captureBtn.disabled = false;
scannerLine.classList.add('hidden');
});
});
});
</script>
@endpush

View File

@ -0,0 +1,244 @@
@extends('layouts.app')
@section('content')
<section class="relative overflow-hidden bg-white px-10 py-24 min-h-[90vh] flex items-center">
<div class="absolute top-0 right-0 -translate-y-1/2 translate-x-1/4 w-96 h-96 bg-orange-100 rounded-full blur-3xl opacity-50"></div>
<div class="absolute bottom-0 left-0 translate-y-1/2 -translate-x-1/4 w-64 h-64 bg-yellow-100 rounded-full blur-3xl opacity-50"></div>
<div class="container mx-auto flex flex-col md:flex-row items-center justify-between relative z-10">
<div class="md:w-1/2 space-y-8">
<span class="px-4 py-2 bg-orange-50 text-orange-700 text-sm font-bold rounded-full tracking-widest uppercase">AI Coffee Technology</span>
<h1 class="text-6xl font-extrabold leading-[1.1] text-gray-900">
Klasifikasi Tingkat <br>
<span class="text-transparent bg-clip-text bg-gradient-to-r from-orange-700 to-amber-500">Kematangan Kopi</span> <br>
Roasted Robusta.
</h1>
<p class="text-gray-500 text-xl max-w-lg leading-relaxed">
Identifikasi kematangan biji kopi Anda (Light, Medium, Dark) dengan akurasi tinggi menggunakan teknologi SVM berbasis Citra Digital.
</p>
<div class="flex flex-wrap gap-4">
@auth
<a href="{{ route('dashboard') }}" class="px-8 py-4 bg-orange-700 text-white rounded-full font-bold shadow-lg hover:bg-orange-800 transition">
Buka Dashboard Anda
</a>
@else
<a href="{{ route('login') }}" class="px-8 py-4 bg-gray-900 text-white rounded-full font-bold shadow-lg hover:bg-black transition">
Mulai Sekarang
</a>
@endauth
<a href="{{ route('karakteristik') }}" class="px-8 py-4 bg-white text-gray-900 border border-gray-200 rounded-full font-bold shadow-sm hover:bg-gray-50 hover:border-gray-300 transition">
Pelajari Karakteristik
</a>
</div>
</div>
<div class="md:w-1/2 mt-16 md:mt-0 flex justify-end relative">
<div class="relative group">
<div class="absolute -inset-4 bg-gradient-to-tr from-orange-500 to-amber-300 rounded-[2.5rem] blur opacity-20 group-hover:opacity-40 transition duration-1000"></div>
<div class="relative bg-white p-4 rounded-[2rem] shadow-2xl overflow-hidden">
<img src="{{ asset('img/roasting-process.jpg') }}" alt="Ilustrasi Kopi Roasted" class="rounded-2xl w-full max-w-[450px] object-cover hover:scale-105 transition-transform duration-700">
</div>
</div>
</div>
</div>
</section>
<section class="bg-gray-900 py-16">
<div class="max-w-6xl mx-auto px-10 grid grid-cols-1 md:grid-cols-3 gap-12 text-center">
<div class="space-y-2">
<h3 class="text-4xl font-black text-orange-500">95%</h3>
<p class="text-white font-bold text-sm uppercase tracking-widest">Akurasi Tinggi</p>
<p class="text-gray-400 text-xs">Dilatih dengan ratusan dataset pilihan</p>
</div>
<div class="space-y-2 border-y md:border-y-0 md:border-x border-gray-800 py-8 md:py-0">
<h3 class="text-4xl font-black text-orange-500">Otomatis</h3>
<p class="text-white font-bold text-sm uppercase tracking-widest">Deteksi Real-time</p>
<p class="text-gray-400 text-xs">Sistem cerdas mengenali warna instan</p>
</div>
<div class="space-y-2">
<h3 class="text-4xl font-black text-orange-500">Praktis</h3>
<p class="text-white font-bold text-sm uppercase tracking-widest">Web-Based</p>
<p class="text-gray-400 text-xs">Akses melalui browser laptop atau smartphone</p>
</div>
</div>
</section>
<section id="informasi" class="py-24 bg-orange-50/50 border-y border-orange-100">
<div class="max-w-6xl mx-auto px-10">
<div class="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
<div class="space-y-6">
<span class="text-orange-700 font-black uppercase text-[10px] tracking-[0.3em]">Core Technology</span>
<h2 class="text-4xl font-extrabold text-gray-900 leading-tight">Mengenal Metode <br> Support Vector Machine.</h2>
<p class="text-gray-600 leading-relaxed text-lg">
Sistem ini menggunakan algoritma SVM untuk mencari hyperplane atau garis pemisah terbaik antara kategori warna biji kopi.
Melalui proses ekstraksi fitur citra digital, setiap data piksel dianalisis secara objektif.
</p>
<div class="flex gap-4">
<div class="p-4 bg-white rounded-2xl shadow-sm border border-orange-100 flex-1">
<h4 class="font-bold text-gray-900 text-sm mb-1">Objektif</h4>
<p class="text-xs text-gray-500">Menghilangkan faktor subjektivitas mata manusia.</p>
</div>
<div class="p-4 bg-white rounded-2xl shadow-sm border border-orange-100 flex-1">
<h4 class="font-bold text-gray-900 text-sm mb-1">Akurat</h4>
<p class="text-xs text-gray-500">Klasifikasi berdasarkan data numerik warna nyata.</p>
</div>
</div>
</div>
<div class="relative">
<div class="bg-white p-6 rounded-[2.5rem] shadow-xl border border-gray-50 rotate-3 hover:rotate-0 transition-transform duration-500">
<div class="aspect-video bg-gray-900 rounded-2xl flex items-center justify-center text-orange-500 font-mono text-xs p-4 overflow-hidden">
SVM Model Output <br>
[Classifying: Dark Roast... 99.2%] <br>
[Result: SUCCESS]
</div>
</div>
</div>
</div>
</div>
</section>
<section id="panduan" class="py-24 bg-white">
<div class="max-w-6xl mx-auto px-10">
<div class="text-center mb-16">
<h2 class="text-4xl font-extrabold text-gray-900">Panduan Pengujian</h2>
<p class="text-gray-500 mt-4 italic">Ikuti prosedur berikut untuk menjamin akurasi klasifikasi citra digital.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="p-10 bg-gray-50 rounded-[2.5rem] border border-gray-100 hover:bg-orange-50 hover:border-orange-200 transition-all duration-300 group">
<div class="w-14 h-14 bg-white text-orange-700 rounded-2xl flex items-center justify-center mb-6 shadow-sm group-hover:scale-110 transition">
<span class="text-xl font-black">01</span>
</div>
<h5 class="font-bold text-xl mb-3 text-gray-900">Alas Putih Polos</h5>
<p class="text-gray-500 text-sm leading-relaxed">
Letakkan biji kopi di atas permukaan putih bersih agar sistem dapat melakukan segmentasi objek dengan sempurna tanpa *noise*.
</p>
</div>
<div class="p-10 bg-gray-50 rounded-[2.5rem] border border-gray-100 hover:bg-orange-50 hover:border-orange-200 transition-all duration-300 group">
<div class="w-14 h-14 bg-white text-orange-700 rounded-2xl flex items-center justify-center mb-6 shadow-sm group-hover:scale-110 transition">
<span class="text-xl font-black">02</span>
</div>
<h5 class="font-bold text-xl mb-3 text-gray-900">Cahaya Terang</h5>
<p class="text-gray-500 text-sm leading-relaxed">
Pastikan lingkungan pengujian memiliki intensitas cahaya yang cukup dan merata untuk menangkap fitur warna asli biji kopi.
</p>
</div>
<div class="p-10 bg-gray-50 rounded-[2.5rem] border border-gray-100 hover:bg-orange-50 hover:border-orange-200 transition-all duration-300 group">
<div class="w-14 h-14 bg-white text-orange-700 rounded-2xl flex items-center justify-center mb-6 shadow-sm group-hover:scale-110 transition">
<span class="text-xl font-black">03</span>
</div>
<h5 class="font-bold text-xl mb-3 text-gray-900">Fokus Kamera</h5>
<p class="text-gray-500 text-sm leading-relaxed">
Posisikan satu biji kopi tepat di tengah kotak bidik. Sistem akan mengekstraksi nilai RGB dari area tersebut secara otomatis.
</p>
</div>
</div>
</div>
</section>
<section class="py-24 bg-gray-50">
<div class="max-w-4xl mx-auto px-10">
<div class="bg-gray-900 rounded-[3rem] p-16 text-center text-white shadow-2xl relative overflow-hidden group">
<div class="relative z-10">
<span class="text-orange-500 font-black uppercase text-[10px] tracking-[0.4em] mb-4 block">Ready to Test?</span>
<h2 class="text-4xl font-bold mb-6">Siap Menguji Biji Kopi Anda?</h2>
<p class="text-gray-400 mb-10 max-w-md mx-auto leading-relaxed">
Gunakan teknologi <strong>SVM</strong> kami untuk standarisasi tingkat kematangan produksi kopi Anda secara instan.
</p>
@auth
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-3 px-10 py-4 bg-orange-700 text-white rounded-2xl font-bold hover:bg-orange-800 hover:-translate-y-1 transition-all shadow-lg">
<span>Buka Dashboard</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path></svg>
</a>
@else
<a href="{{ route('login') }}" class="inline-flex items-center gap-3 px-10 py-4 bg-orange-700 text-white rounded-2xl font-bold hover:bg-orange-800 hover:-translate-y-1 transition-all shadow-lg text-sm uppercase tracking-widest">
<span>Mulai Sekarang</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path></svg>
</a>
@endauth
</div>
<div class="absolute top-0 right-0 w-64 h-64 bg-orange-700/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl group-hover:bg-orange-700/20 transition duration-1000"></div>
<div class="absolute bottom-0 left-0 w-48 h-48 bg-white/5 rounded-full translate-y-1/2 -translate-x-1/2 blur-2xl"></div>
</div>
</div>
</section>
<footer class="py-12 bg-white border-t border-gray-100">
<div class="max-w-6xl mx-auto px-10 flex flex-col md:flex-row justify-between items-center gap-6">
<div class="flex items-center gap-2">
<div class="w-8 h-8 bg-orange-700 rounded-lg"></div>
<div class="text-xl font-bold italic tracking-tighter">RobustaMeter.id</div>
</div>
<p class="text-gray-400 text-xs">© 2026 RobustaMeter Tugas Akhir Klasifikasi SVM Berbasis Citra Digital.</p>
<div class="flex gap-6 text-[10px] font-black uppercase tracking-widest text-gray-500">
<a href="{{ route('landingpage') }}" class="hover:text-orange-700 transition">Beranda</a>
<a href="{{ route('karakteristik') }}" class="hover:text-orange-700 transition">Karakteristik</a>
<a href="#informasi" class="hover:text-orange-700 transition">Informasi</a>
<a href="{{ route('dashboard') }}" class="hover:text-orange-700 transition">Panduan</a>
</div>
</div>
</footer>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const slides = document.querySelectorAll('.slide');
const dots = document.querySelectorAll('.dot');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
let currentIndex = 0;
if (!nextBtn || !prevBtn) return;
function showSlide(index) {
slides.forEach((slide, i) => {
slide.classList.remove('opacity-100', 'scale-100');
slide.classList.add('opacity-0', 'scale-105');
dots[i].classList.remove('bg-orange-700', 'w-10');
dots[i].classList.add('bg-gray-300', 'w-3');
});
slides[index].classList.remove('opacity-0', 'scale-105');
slides[index].classList.add('opacity-100', 'scale-100');
dots[index].classList.remove('bg-gray-300', 'w-3');
dots[index].classList.add('bg-orange-700', 'w-10');
}
nextBtn.addEventListener('click', () => {
currentIndex = (currentIndex + 1) % slides.length;
showSlide(currentIndex);
});
prevBtn.addEventListener('click', () => {
currentIndex = (currentIndex - 1 + slides.length) % slides.length;
showSlide(currentIndex);
});
let autoSlide = setInterval(() => {
currentIndex = (currentIndex + 1) % slides.length;
showSlide(currentIndex);
}, 6000);
const resetInterval = () => {
clearInterval(autoSlide);
autoSlide = setInterval(() => {
currentIndex = (currentIndex + 1) % slides.length;
showSlide(currentIndex);
}, 6000);
};
nextBtn.addEventListener('click', resetInterval);
prevBtn.addEventListener('click', resetInterval);
});
</script>
@endpush

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RobustaMeter - Klasifikasi Kematangan Kopi</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Poppins', sans-serif; }
html {
scroll-behavior: smooth;
}
</style>
</head>
<body class="bg-white text-gray-900">
<nav class="flex justify-between items-center px-10 py-6 border-b">
<div class="text-xl font-bold italic text-gray-900">RobustaMeter.id</div>
<div class="space-x-8 text-sm font-medium">
<a href="{{ route('landingpage') }}" class="hover:text-orange-600 transition">Beranda</a>
<a href="{{ route('karakteristik') }}" class="hover:text-orange-600 transition">Karakteristik</a>
<a href="#informasi" class="hover:text-orange-600 transition">Informasi</a>
<a href="#panduan" class="hover:text-orange-600 transition">Panduan</a>
</div>
<div>
@guest
<a href="{{ route('login') }}" class="px-6 py-2 border border-black rounded-full hover:bg-black hover:text-white transition">
Masuk
</a>
@endguest
@auth
<div class="flex items-center gap-6">
<div class="flex flex-col items-end">
<span class="text-[10px] font-black uppercase text-gray-400 tracking-widest">Logged in as</span>
<span class="text-sm font-bold text-gray-900">{{ Auth::user()->name }}</span>
</div>
<form action="{{ route('logout') }}" method="POST" class="inline">
@csrf
<button type="submit" class="px-5 py-2 bg-gray-100 text-gray-600 text-xs font-bold rounded-full hover:bg-red-50 hover:text-red-600 transition">
Keluar
</button>
</form>
</div>
@endauth
</div>
</nav>
<main>
@yield('content')
</main>
@stack('scripts')
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,18 +1,77 @@
<?php <?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Hash;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\ClassificationController;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Web Routes | Web Routes
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/ */
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); return view('landingpage');
})->name('landingpage');
Route::get('/karakteristik', [ClassificationController::class, 'karakteristik'])->name('karakteristik');
Route::middleware('guest')->group(function () {
// Login
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
// Register
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
Route::post('/register', [AuthController::class, 'register']);
// Lupa Password
Route::get('/forgot-password', function() {
return view('auth.forgot-password');
})->name('password.request');
Route::post('/forgot-password', function(Request $request) {
$request->validate([
'email' => 'required|email|exists:users,email',
'password' => 'required|min:8|confirmed',
], [
'email.exists' => 'Email tidak ditemukan dalam sistem.',
'password.min' => 'Password minimal 8 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
]);
$user = \App\Models\User::where('email', $request->email)->first();
$user->update([
'password' => Hash::make($request->password)
]);
return redirect()->route('login')->with('status', 'Password berhasil diubah! Silakan login.');
})->name('password.update.direct');
}); });
Route::middleware('auth')->group(function () {
// Dashboard
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
// Klasifikasi
Route::get('/klasifikasi', function () {
return view('klasifikasi');
})->name('klasifikasi');
// History
Route::get('/history', [ClassificationController::class, 'history'])->name('history');
Route::delete('/classification/{id}', [ClassificationController::class, 'destroy'])->name('classification.destroy');
// API
Route::post('/api/destroy-temp', [ClassificationController::class, 'destroyTemp'])->name('api.destroy.temp');
// Logout
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
});
Route::post('/api/classify', [ClassificationController::class, 'process'])->name('api.classify');
Route::post('/api/save-result', [ClassificationController::class, 'store'])->name('api.save');

BIN
test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB