Add user classification UI and predict flow

This commit is contained in:
kizadl 2026-04-28 00:22:25 +07:00
parent fa3902fcea
commit e639b0ac57
10 changed files with 446 additions and 76 deletions

View File

@ -6,27 +6,73 @@
use App\Models\Classification; use App\Models\Classification;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class ClassificationController extends Controller class ClassificationController extends Controller
{ {
public function store(Request $request) public function index()
{
$classifications = Classification::where('user_id', auth()->id())->get();
return inertia('user/classification/ClassificationIndex', [
'classifications' => $classifications,
]);
}
public function predict(Request $request)
{ {
$request->validate([ $request->validate([
'image' => 'required|image|max:2048', 'image' => 'required|image|max:2048',
'result' => 'required|string',
'confidence' => 'required',
]); ]);
// Simpan gambar ke storage public $image = $request->file('image');
$path = $request->file('image')->store('classifications', 'public');
try {
// 1. Kirim ke AI Server untuk prediksi
$response = Http::attach(
'image',
file_get_contents($image),
$image->getClientOriginalName()
)->post('http://127.0.0.1:5001/predict');
if ($response->failed()) {
return response()->json(['error' => 'Gagal terhubung ke AI server'], 500);
}
$data = $response->json();
// 2. Jika sukses, simpan gambar dan hasil ke database
$path = $image->store('classifications', 'public');
// Simpan ke Database
Classification::create([ Classification::create([
'user_id' => auth()->id(), // null jika guest, id jika login 'user_id' => auth()->id() ?? null,
'image_path' => $path, 'image_path' => $path,
'result' => $request->result, 'result' => $data['label'],
'confidence' => (float) filter_var($request->confidence, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION), 'confidence' => (float) filter_var($data['confidence'], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION),
]); ]);
return response()->json(['message' => 'Data tersimpan ke history']); return response()->json([
'label' => $data['label'],
'confidence' => $data['confidence'],
'message' => 'Hasil klasifikasi berhasil disimpan'
]);
} catch (\Exception $e) {
return response()->json(['error' => 'Terjadi kesalahan: ' . $e->getMessage()], 500);
}
}
public function destroy(Classification $classification)
{
if ($classification->user_id !== auth()->id()) {
abort(403);
}
// Hapus file dari storage jika ada
if ($classification->image_path && \Storage::disk('public')->exists($classification->image_path)) {
\Storage::disk('public')->delete($classification->image_path);
}
$classification->delete();
return back()->with('success', 'Klasifikasi berhasil dihapus');
} }
} }

View File

@ -20,6 +20,8 @@ createInertiaApp({
return [AppLayout, SettingsLayout]; return [AppLayout, SettingsLayout];
case name.startsWith('admin/'): case name.startsWith('admin/'):
return [AppLayout]; return [AppLayout];
case name.startsWith('user/'):
return [AppLayout];
default: default:
return PublicLayout; return PublicLayout;
} }

View File

@ -2,7 +2,7 @@
import { Link, usePage } from '@inertiajs/vue3'; import { Link, usePage } from '@inertiajs/vue3';
import { computed } from 'vue'; import { computed } from 'vue';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
import { BookOpen, FolderGit2, LayoutGrid, Users } from 'lucide-vue-next'; import { BookOpen, FolderGit2, LayoutGrid, Users, ScanSearch } from 'lucide-vue-next';
import AppLogo from '@/components/AppLogo.vue'; import AppLogo from '@/components/AppLogo.vue';
import NavFooter from '@/components/NavFooter.vue'; import NavFooter from '@/components/NavFooter.vue';
import NavMain from '@/components/NavMain.vue'; import NavMain from '@/components/NavMain.vue';
@ -23,8 +23,7 @@ const page = usePage();
// Ambil data user dari auth yang dikirim via HandleInertiaRequests // Ambil data user dari auth yang dikirim via HandleInertiaRequests
const user = computed(() => page.props.auth.user); const user = computed(() => page.props.auth.user);
// 1. Definisikan semua menu, tambahkan properti 'role' untuk menu yang dibatasi const adminNavItems: NavItem[] = [
const allNavItems: NavItem[] = [
{ {
title: 'Dashboard', title: 'Dashboard',
href: route('admin.dashboard'), href: route('admin.dashboard'),
@ -34,20 +33,41 @@ const allNavItems: NavItem[] = [
title: 'Users Management', title: 'Users Management',
href: route('admin.users.index'), href: route('admin.users.index'),
icon: Users, icon: Users,
role: 'admin', // Hanya untuk admin
}, },
]; ];
// 2. Filter menu berdasarkan role user yang sedang login const userNavItems: NavItem[] = [
const filteredNavItems = computed(() => { {
return allNavItems.filter(item => { title: 'Dashboard',
// Jika menu tidak punya batasan role, tampilkan untuk semua href: route('dashboard'),
if (!item.role) return true; icon: LayoutGrid,
// Jika ada batasan role, cek apakah role user cocok (admin === admin) },
return item.role === user.value?.role; {
}); title: 'Klasifikasi',
href: route('classifications.index'),
icon: ScanSearch,
},
];
const navItems = computed(() => {
if (user.value?.role === 'admin') {
return adminNavItems;
} else {
return userNavItems;
}
}); });
// 2. Filter menu berdasarkan role user yang sedang login
// const filteredNavItems = computed(() => {
// return allNavItems.filter(item => {
// // Jika menu tidak punya batasan role, tampilkan untuk semua
// if (!item.role) return true;
// // Jika ada batasan role, cek apakah role user cocok (admin === admin)
// return item.role === user.value?.role;
// });
// });
const footerNavItems: NavItem[] = [ const footerNavItems: NavItem[] = [
{ {
title: 'Repository', title: 'Repository',
@ -77,7 +97,7 @@ const footerNavItems: NavItem[] = [
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain :items="filteredNavItems" /> <NavMain :items="navItems" />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>

View File

@ -4,11 +4,14 @@
<h2 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6 tracking-tight font-display"> <h2 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6 tracking-tight font-display">
Pantau Riwayat Klasifikasi Pantau Riwayat Klasifikasi
</h2> </h2>
<p class="text-lg sm:text-xl text-zinc-400 mb-10 max-w-2xl mx-auto"> <p v-if="!user" class="text-lg sm:text-xl text-zinc-400 mb-10 max-w-2xl mx-auto">
Masuk ke akun Anda sekarang untuk menyimpan hasil dan melihat kembali seluruh riwayat klasifikasi biji kopi yang pernah Anda lakukan. Masuk ke akun Anda sekarang untuk menyimpan hasil dan melihat kembali seluruh riwayat klasifikasi biji kopi yang pernah Anda lakukan.
</p> </p>
<p v-else class="text-lg sm:text-xl text-zinc-400 mb-10 max-w-2xl mx-auto">
Pantau riwayat klasifikasi biji kopi Anda sekarang.
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4"> <div v-if="!user" class="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link <Link
href="/login" href="/login"
class="shimmer-btn bg-emerald-500 text-zinc-950 hover:bg-emerald-400 rounded-full px-8 py-3 text-base font-bold shadow-lg shadow-emerald-500/20 flex items-center gap-2 transition-all active:scale-95" class="shimmer-btn bg-emerald-500 text-zinc-950 hover:bg-emerald-400 rounded-full px-8 py-3 text-base font-bold shadow-lg shadow-emerald-500/20 flex items-center gap-2 transition-all active:scale-95"
@ -24,6 +27,15 @@
</Link> </Link>
</div> </div>
<div v-else class="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href="/dashboard"
class="shimmer-btn bg-emerald-500 text-zinc-950 hover:bg-emerald-400 rounded-full px-8 py-3 text-base font-bold shadow-lg shadow-emerald-500/20 flex items-center gap-2 transition-all active:scale-95"
>
Dashboard
</Link>
</div>
<p class="mt-8 text-sm text-zinc-500">Gratis sepenuhnya. Jangan biarkan riwayat klasifikasi Anda hilang.</p> <p class="mt-8 text-sm text-zinc-500">Gratis sepenuhnya. Jangan biarkan riwayat klasifikasi Anda hilang.</p>
</div> </div>
</section> </section>
@ -31,5 +43,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowRight } from 'lucide-vue-next' import { ArrowRight } from 'lucide-vue-next'
import { Link } from '@inertiajs/vue3' import { Link, usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
const page = usePage()
const user = computed(() => page.props.auth?.user)
</script> </script>

View File

@ -31,6 +31,24 @@
<!-- CTA Buttons --> <!-- CTA Buttons -->
<div class="hidden md:flex items-center gap-3"> <div class="hidden md:flex items-center gap-3">
<div v-if="user">
<Link
href="/dashboard"
class="px-4 py-2 text-sm text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors"
>
Dashboard
</Link>
<Link
class="shimmer-btn bg-red-500 text-zinc-950 hover:bg-red-400 rounded-full px-4 py-2 text-sm font-bold shadow-lg shadow-emerald-500/20 transition-all active:scale-95 inline-flex items-center"
:href="route('logout')"
method="post"
@click="handleLogout"
>
<LogOut class="mr-2 h-4 w-4" />
Log out
</Link>
</div>
<div v-else>
<Link <Link
href="/login" href="/login"
class="px-4 py-2 text-sm text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors" class="px-4 py-2 text-sm text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors"
@ -44,6 +62,7 @@
Daftar Daftar
</Link> </Link>
</div> </div>
</div>
<!-- Mobile Menu Button --> <!-- Mobile Menu Button -->
<button <button
@ -88,9 +107,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { computed, ref } from 'vue'
import { Menu, X } from 'lucide-vue-next' import { Link, usePage, router } from '@inertiajs/vue3'
import { Link } from '@inertiajs/vue3' import { LogOut, Menu, X } from 'lucide-vue-next'
import { route } from 'ziggy-js';
const page = usePage()
const user = computed(() => page.props.auth?.user)
const navItems = [ const navItems = [
{ label: 'Klasifikasi', href: '#klasifikasi' }, { label: 'Klasifikasi', href: '#klasifikasi' },
@ -99,6 +122,10 @@ const navItems = [
{ label: 'F.A.Q', href: '#faq' }, { label: 'F.A.Q', href: '#faq' },
] ]
const handleLogout = () => {
router.flushAll();
};
const hoveredIndex = ref<number | null>(null) const hoveredIndex = ref<number | null>(null)
const mobileMenuOpen = ref(false) const mobileMenuOpen = ref(false)
</script> </script>

View File

@ -98,6 +98,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { router } from '@inertiajs/vue3'
import { route } from 'ziggy-js'
import { Upload, Plus, X, Loader2, Bean } from 'lucide-vue-next' import { Upload, Plus, X, Loader2, Bean } from 'lucide-vue-next'
const imagePreview = ref<string | null>(null) const imagePreview = ref<string | null>(null)
@ -128,27 +130,37 @@ const startClassification = async () => {
formData.append('image', selectedFile.value) formData.append('image', selectedFile.value)
try { try {
const response = await fetch('http://127.0.0.1:5001/predict', { const response = await fetch('/predict', {
method: 'POST', method: 'POST',
body: formData body: formData,
headers: {
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content || ''
}
}) })
if (!response.ok) throw new Error("Gagal terhubung ke AI server") if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || "Gagal melakukan klasifikasi")
}
const data = await response.json() const data = await response.json()
// Pastikan data yang dikirim Flask sesuai (label & confidence)
predictionResult.value = { predictionResult.value = {
label: data.label, label: data.label,
confidence: data.confidence confidence: data.confidence
} }
} catch (error) {
alert("Error: Pastikan backend-ai sudah dijalankan (python app.py)") // Refresh data jika di Inertia context (dashboard)
if (typeof route === 'function' && route().current('classifications.index')) {
router.reload({ only: ['classifications'] })
}
} catch (error: any) {
console.error(error) console.error(error)
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
} }
const clearImage = () => { const clearImage = () => {
imagePreview.value = null imagePreview.value = null
selectedFile.value = null selectedFile.value = null

View File

@ -23,7 +23,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, computed } from 'vue' import { ref, computed } from 'vue'
import { usePage } from '@inertiajs/vue3' import { usePage } from '@inertiajs/vue3'
import { import {
Dialog, Dialog,
@ -42,30 +42,61 @@ const flash = computed(() => page.props.flash || {})
const message = ref(null) const message = ref(null)
const type = ref(null) const type = ref(null)
const isOpen = ref(false) const isOpen = ref(false)
let activeTimeout = null
watch( const trigger = (msg, t) => {
flash, if (activeTimeout) clearTimeout(activeTimeout)
() => {
if (flash.value.success) {
message.value = flash.value.success
type.value = 'success'
isOpen.value = true
} else if (flash.value.error) {
message.value = flash.value.error
type.value = 'error'
isOpen.value = true
}
if (message.value) { message.value = msg
setTimeout(() => { type.value = t
isOpen.value = true
activeTimeout = setTimeout(() => {
isOpen.value = false isOpen.value = false
setTimeout(() => { setTimeout(() => {
message.value = null message.value = null
type.value = null type.value = null
activeTimeout = null
}, 300) }, 300)
}, 2500) }, 2500)
}
const showNotification = (msg, t) => {
if (!msg) return
// Jika sudah terbuka, kita reset agar animasinya terpicu ulang
if (isOpen.value) {
isOpen.value = false
setTimeout(() => trigger(msg, t), 100)
} else {
trigger(msg, t)
} }
}, }
{ immediate: true }
) // Gunakan router event agar selalu terdeteksi setiap kali request selesai
import { router } from '@inertiajs/vue3'
import { onMounted, onUnmounted } from 'vue'
let unregisterFinishEvent = null
onMounted(() => {
// Cek saat mount awal
if (flash.value.success || flash.value.error) {
showNotification(flash.value.success || flash.value.error, flash.value.success ? 'success' : 'error')
}
// Dengarkan setiap kali aksi Inertia selesai
unregisterFinishEvent = router.on('finish', () => {
const currentFlash = page.props.flash || {}
if (currentFlash.success || currentFlash.error) {
showNotification(currentFlash.success || currentFlash.error, currentFlash.success ? 'success' : 'error')
}
})
})
onUnmounted(() => {
if (unregisterFinishEvent) unregisterFinishEvent()
})
</script> </script>

View File

@ -0,0 +1,204 @@
<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { Button } from '@/components/ui/button';
import { Trash2, Plus, History, ScanSearch, Calendar, AlertCircle } from 'lucide-vue-next';
import Scanner from '@/components/public/Scanner.vue';
import { ref } from 'vue';
import { route } from 'ziggy-js';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
defineOptions({
layout: {
breadcrumbs: [
{
title: 'Klasifikasi',
href: route('classifications.index'),
},
],
},
});
const props = defineProps<{
classifications: any[];
}>();
const showScanner = ref(false);
const isDeleteDialogOpen = ref(false);
const itemToDelete = ref<number | null>(null);
const isDeleting = ref(false);
const confirmDelete = (id: number) => {
itemToDelete.value = id;
isDeleteDialogOpen.value = true;
};
const deleteClassification = () => {
if (!itemToDelete.value) return;
isDeleting.value = true;
router.delete(route('classifications.destroy', itemToDelete.value), {
preserveScroll: true,
onSuccess: () => {
isDeleteDialogOpen.value = false;
itemToDelete.value = null;
},
onFinish: () => {
isDeleting.value = false;
}
});
};
const toggleScanner = () => {
showScanner.value = !showScanner.value;
if (!showScanner.value) {
// Refresh data saat menutup scanner jika ada data baru
router.reload({ only: ['classifications'] });
}
};
</script>
<template>
<Head title="Klasifikasi Kopi" />
<div class="flex h-full flex-1 flex-col gap-8 rounded-xl p-4 md:p-8 overflow-x-hidden">
<!-- Header Section -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 class="text-3xl font-bold tracking-tight flex items-center gap-3">
<History class="w-8 h-8 text-emerald-500" />
Riwayat Klasifikasi
</h1>
<p class="text-muted-foreground mt-1">Kelola dan lihat hasil analisis biji kopi Anda.</p>
</div>
<Button
@click="toggleScanner"
:variant="showScanner ? 'outline' : 'default'"
class="w-full sm:w-auto h-12 px-6 rounded-xl font-bold shadow-lg shadow-emerald-500/20 transition-all active:scale-95"
>
<template v-if="!showScanner">
<Plus class="mr-2 h-5 w-5" />
Klasifikasi Baru
</template>
<template v-else>
Tutup Scanner
</template>
</Button>
</div>
<!-- Scanner Section -->
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="transform -translate-y-4 opacity-0"
enter-to-class="transform translate-y-0 opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="transform translate-y-0 opacity-100"
leave-to-class="transform -translate-y-4 opacity-0"
>
<div v-if="showScanner" class="mb-12 rounded-3xl overflow-hidden shadow-2xl border border-emerald-500/20">
<Scanner />
</div>
</transition>
<!-- History Grid -->
<div v-if="classifications.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pb-12">
<div
v-for="item in classifications"
:key="item.id"
class="group relative bg-white dark:bg-zinc-900 rounded-[2rem] border border-zinc-200 dark:border-zinc-800 p-4 flex gap-5 items-center shadow-sm hover:shadow-xl hover:border-emerald-500/30 transition-all duration-300"
>
<!-- Small Photo Thumbnail -->
<div class="w-24 h-24 sm:w-32 sm:h-32 rounded-2xl overflow-hidden bg-zinc-100 dark:bg-zinc-950 flex-shrink-0 relative">
<img
:src="'/storage/' + item.image_path"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
<!-- Info Section -->
<div class="flex-1 min-w-0 flex flex-col justify-between py-1 pr-6">
<div class="flex justify-between items-start mb-2">
<div class="space-y-0.5">
<div class="flex items-center gap-2 text-[9px] font-bold text-zinc-400 uppercase tracking-widest mb-1">
<Calendar class="w-3 h-3" />
{{ new Date(item.created_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'short' }) }}
</div>
<h3 class="text-xl font-black text-emerald-600 dark:text-emerald-500 italic truncate leading-none">{{ item.result }}</h3>
</div>
<div class="text-right">
<p class="text-[9px] font-bold text-zinc-400 uppercase tracking-widest">Confidence</p>
<p class="text-lg font-bold text-zinc-900 dark:text-zinc-100 leading-none mt-1">{{ Number(item.confidence).toFixed(2) }}%</p>
</div>
</div>
<div class="space-y-2">
<div class="h-1.5 w-full bg-zinc-100 dark:bg-zinc-800 rounded-full overflow-hidden">
<div
class="h-full bg-emerald-500 rounded-full transition-all duration-1000"
:style="{ width: item.confidence + '%' }"
></div>
</div>
</div>
</div>
<!-- Tombol Hapus Pojok Kanan Bawah -->
<Button
variant="ghost"
size="icon"
@click="confirmDelete(item.id)"
class="absolute bottom-3 right-3 h-8 w-8 text-zinc-400 hover:text-red-500 hover:bg-red-50/50 dark:hover:bg-red-950/20 transition-colors"
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Empty State -->
<div v-else-if="!showScanner" class="text-center py-32 bg-white dark:bg-zinc-900 rounded-[2.5rem] border-2 border-dashed border-zinc-200 dark:border-zinc-800 shadow-sm mb-12">
<div class="inline-flex p-6 rounded-3xl bg-zinc-50 dark:bg-zinc-950 mb-6">
<ScanSearch class="w-12 h-12 text-zinc-400" />
</div>
<h3 class="text-xl font-bold text-zinc-900 dark:text-white mb-2">Belum ada riwayat</h3>
<p class="text-zinc-500 dark:text-zinc-400 max-w-xs mx-auto mb-8">Anda belum pernah melakukan klasifikasi biji kopi. Mulai sekarang untuk melihat hasilnya di sini.</p>
<Button @click="showScanner = true" class="h-12 px-8 rounded-xl font-bold">
Mulai Klasifikasi Pertama
</Button>
</div>
<!-- Delete Confirmation Dialog -->
<Dialog :open="isDeleteDialogOpen" @update:open="isDeleteDialogOpen = $event">
<DialogContent class="sm:max-w-[425px] rounded-3xl">
<DialogHeader>
<div class="mx-auto w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center mb-4">
<AlertCircle class="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<DialogTitle class="text-center text-xl font-bold">Hapus Riwayat?</DialogTitle>
<DialogDescription class="text-center">
Tindakan ini tidak dapat dibatalkan. Riwayat klasifikasi dan foto akan dihapus secara permanen dari server kami.
</DialogDescription>
</DialogHeader>
<DialogFooter class="flex flex-col sm:flex-row gap-2 mt-4">
<Button variant="outline" @click="isDeleteDialogOpen = false" class="rounded-xl flex-1 h-12 font-bold">
Batal
</Button>
<Button
variant="destructive"
@click="deleteClassification"
:disabled="isDeleting"
class="rounded-xl flex-1 h-12 font-bold"
>
{{ isDeleting ? 'Menghapus...' : 'Ya, Hapus' }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
{{-- Inline script to detect system dark mode preference and apply it immediately --}} {{-- Inline script to detect system dark mode preference and apply it immediately --}}
<script> <script>

View File

@ -6,7 +6,12 @@
use App\Http\Controllers\Admin\DashboardController; use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Public\ClassificationController; // Import Controller use App\Http\Controllers\Public\ClassificationController; // Import Controller
Route::redirect('dashboard', 'admin/dashboard')->name('dashboard'); Route::get('dashboard', function () {
if (auth()->user()->role === 'admin') {
return redirect()->route('admin.dashboard');
}
return app(DashboardController::class)->index();
})->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/', function () { Route::get('/', function () {
return inertia('public/Home', [ return inertia('public/Home', [
@ -14,7 +19,13 @@
]); ]);
})->name('home'); })->name('home');
Route::post('/classifications', [ClassificationController::class, 'store'])->name('public.classifications.store'); Route::post('/predict', [ClassificationController::class, 'predict'])->name('public.predict');
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/classifications', [ClassificationController::class, 'index'])->name('classifications.index');
Route::delete('/classifications/{classification}', [ClassificationController::class, 'destroy'])->name('classifications.destroy');
});
Route::prefix('admin')->name('admin.')->middleware(['auth', 'verified'])->group(function () { Route::prefix('admin')->name('admin.')->middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard'); Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');