kopi1
This commit is contained in:
parent
718760931d
commit
0c4ac2449d
|
|
@ -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('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'Asia/Jakarta',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 393 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Binary file not shown.
|
|
@ -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}")
|
||||||
Binary file not shown.
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
|
@ -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');
|
||||||
Loading…
Reference in New Issue