nambah menu profil tok

This commit is contained in:
MufridaFaraDiani27 2026-05-03 02:08:55 +07:00
parent 84b66d4465
commit 227aad6557
16 changed files with 734 additions and 367 deletions

View File

@ -29,7 +29,7 @@ public function index(Request $request)
$query->where('wisata', 'LIKE', '%' . $request->wisata . '%');
}
$mentah = $query->latest()->paginate(50)->withQueryString();
$mentah = $query->latest()->paginate(20)->withQueryString();
// OPTIONAL
$preprocessing = PreprocessingData::latest()->paginate(30, ['*'], 'pre_page');
@ -37,7 +37,7 @@ public function index(Request $request)
// 🔥 RINGKASAN DATA
$totalUlasan = Ulasan::count();
$totalWisata = Ulasan::distinct('wisata')->count('wisata');
$lastUpdate = Ulasan::max('updated_at');
$lastUpdate = \App\Models\Ulasan::latest()->value('created_at');
return view('ulasan.index', compact(
'mentah',

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -5,136 +5,311 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - SENTARA</title>
@vite(['resources/css/app.css','resources/js/app.js'])
<link rel="icon" type="image/png" href="{{ asset('favicon.png') }}">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*{
margin:0;
padding:0;
box-sizing:border-box;
font-family:'Poppins',sans-serif;
}
body{
background:#f3f5fb;
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:40px;
}
/* CONTAINER */
.container{
width:1350px;
max-width:100%;
height:720px;
background:#fff;
border-radius:25px;
display:flex;
overflow:hidden;
box-shadow:0 30px 80px rgba(0,0,0,0.1);
}
/* LEFT */
.left{
width:50%;
padding:60px 80px;
display:flex;
flex-direction:column;
justify-content:center;
}
/* LOGO */
.logo{
text-align:center;
margin-bottom:25px;
}
.logo img{
width:200px;
}
/* TITLE */
.title{
text-align:center;
font-size:28px;
font-weight:600;
}
.title span{
color:#ff5c8d;
}
.subtitle{
text-align:center;
font-size:14px;
color:#777;
margin:12px 0 35px;
}
/* FORM */
.form-group{
margin-bottom:20px;
}
label{
font-size:13px;
font-weight:500;
}
input{
width:100%;
padding:14px;
border-radius:12px;
border:1px solid #ddd;
margin-top:6px;
font-size:14px;
}
.input-group{
position:relative;
}
.toggle{
position:absolute;
right:12px;
top:14px;
cursor:pointer;
}
.forgot{
text-align:right;
margin-top:6px;
}
.forgot a{
font-size:12px;
color:#2d5be3;
text-decoration:none;
}
/* BUTTON */
.btn{
width:100%;
padding:14px;
border:none;
border-radius:12px;
background:linear-gradient(90deg,#2d5be3,#4c7dff);
color:#fff;
font-weight:500;
margin-top:20px;
cursor:pointer;
transition:0.3s;
}
.btn:hover{
opacity:0.9;
}
.right{
width:50%;
position:relative;
overflow:hidden;
}
/* GAMBAR */
.right img{
position:absolute;
top:0;
right:0;
width:100%;
height:100%;
object-fit:cover;
/* SHAPE DIPERBAIKI */
clip-path: ellipse(120% 100% at 100% 50%);
}
/* OVERLAY */
.overlay{
position:absolute;
width:100%;
height:100%;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
text-align:center;
color:#fff;
background:rgba(0,0,0,0.25);
padding:20px;
z-index:2; /* DI ATAS */
}
/* QUOTE */
.quote{
font-size:42px;
color:#ff5c8d;
margin-bottom:10px;
}
/* TEXT */
.overlay p{
font-size:30px;
font-weight:500;
line-height:1.5;
max-width:420px;
}
.overlay span{
color:#ff5c8d;
font-weight:600;
}
/* GARIS */
.line{
width:70px;
height:3px;
background:#fff;
border-radius:10px;
margin-top:18px;
position:relative;
}
.line::after{
content:'';
position:absolute;
width:12px;
height:12px;
border:2px solid #fff;
border-left:none;
border-top:none;
transform:rotate(-45deg);
right:-12px;
top:-5px;
}
/* MOBILE */
@media(max-width:900px){
.container{
flex-direction:column;
height:auto;
}
.left{
width:100%;
padding:40px;
}
.right{
width:100%;
height:260px;
}
.overlay p{
font-size:20px;
}
.logo img{
width:150px;
}
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center bg-gray-100 px-6">
<body>
<!-- WRAPPER CARD -->
<div class="w-full max-w-6xl bg-white rounded-3xl shadow-2xl overflow-hidden flex">
<div class="container">
<!-- LEFT SIDE LOGIN -->
<div class="w-1/2 flex flex-col justify-center px-14 py-16 bg-white">
<!-- LEFT -->
<div class="left">
<h1 class="text-4xl font-bold text-blue-900 mb-2">
SENTARA
</h1>
<div class="logo">
<img src="{{ asset('images/logo-sentara.png') }}">
</div>
<p class="text-gray-500 mb-10">
<div class="title">
Selamat Datang <span>Kembali!</span>
</div>
<div class="subtitle">
Login untuk mengakses Dashboard Analisis Sentimen Wisata Jember
</p>
</div>
<!-- Login Form -->
<form method="POST" action="{{ route('login') }}" class="space-y-6">
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Email -->
<div>
<label class="block text-gray-700 font-medium mb-2">
Email
</label>
<input type="email" name="email" required
class="w-full border border-gray-300 rounded-xl px-4 py-3
focus:ring-2 focus:ring-blue-400 focus:outline-none">
<div class="form-group">
<label>Email</label>
<input type="email" name="email" placeholder="Masukkan email Anda" required>
</div>
<!-- Password -->
<div>
<label class="block text-gray-700 font-medium mb-2">
Password
</label>
<input type="password" name="password" required
class="w-full border border-gray-300 rounded-xl px-4 py-3
focus:ring-2 focus:ring-blue-400 focus:outline-none">
<div class="form-group">
<label>Password</label>
<div class="input-group">
<input type="password" id="password" name="password" placeholder="Masukkan password Anda" required>
<span class="toggle" onclick="togglePass()">👁</span>
</div>
<!-- Button -->
<button type="submit"
class="w-full bg-blue-700 hover:bg-blue-800 text-white py-3
rounded-xl font-semibold shadow-md transition">
Login
</button>
<div class="forgot">
<a href="{{ route('password.request') }}">Lupa password?</a>
</div>
</div>
<button class="btn">Login</button>
<!-- Register Link -->
<p class="text-sm text-gray-500 text-center">
Belum punya akun?
<a href="{{ route('register') }}"
class="text-blue-700 font-semibold hover:underline">
Register
</a>
</p>
</form>
</div>
<!-- RIGHT -->
<div class="right">
<!-- RIGHT SIDE SLIDER -->
<div class="w-1/2 relative overflow-hidden">
<img src="{{ asset('images/papuma.jpg') }}" alt="bg">
<!-- Slides -->
<img src="{{ asset('images/papuma.jpg') }}"
class="slide absolute inset-0 w-full h-full object-cover">
<div class="overlay">
<img src="{{ asset('images/watuulo.jpeg') }}"
class="slide hidden absolute inset-0 w-full h-full object-cover">
<p>
Memahami opini,<br>
meningkatkan <span>pariwisata</span><br>
Jember.
</p>
<img src="{{ asset('images/botani.jpg') }}"
class="slide hidden absolute inset-0 w-full h-full object-cover">
<img src="{{ asset('images/gununggambir.jpeg') }}"
class="slide hidden absolute inset-0 w-full h-full object-cover">
<!-- Overlay -->
<div class="absolute inset-0 bg-black/40 flex items-center justify-center">
<h2 class="text-white text-3xl font-bold text-center px-10">
Eksplorasi Wisata Kabupaten Jember
</h2>
</div>
<!-- LEFT ARROW -->
<button onclick="prevSlide()"
class="absolute left-5 top-1/2 -translate-y-1/2
bg-white/40 hover:bg-white/70 text-white
px-4 py-2 rounded-full text-2xl font-bold transition">
</button>
<!-- RIGHT ARROW -->
<button onclick="nextSlide()"
class="absolute right-5 top-1/2 -translate-y-1/2
bg-white/40 hover:bg-white/70 text-white
px-4 py-2 rounded-full text-2xl font-bold transition">
</button>
</div>
</div>
</div>
<!-- SLIDER SCRIPT -->
<script>
let index = 0;
const slides = document.querySelectorAll(".slide");
function showSlide(i) {
slides.forEach(slide => slide.classList.add("hidden"));
slides[i].classList.remove("hidden");
function togglePass(){
let x = document.getElementById("password");
x.type = x.type === "password" ? "text" : "password";
}
function nextSlide() {
index = (index + 1) % slides.length;
showSlide(index);
}
function prevSlide() {
index = (index - 1 + slides.length) % slides.length;
showSlide(index);
}
// // Auto Slide setiap 3 detik
// setInterval(nextSlide, 3000);
</script>
</body>

View File

@ -88,7 +88,7 @@ class="block hover:text-gray-300">
<!-- Page Content -->
<main class="p-6">
{{ $slot }}
@yield('content')
</main>
</div>

View File

@ -35,7 +35,7 @@
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
<x-dropdown-link :href="route('profile')">
{{ __('Profile') }}
</x-dropdown-link>
@ -81,7 +81,7 @@
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
<x-responsive-nav-link :href="route('profile')">
{{ __('Profile') }}
</x-responsive-nav-link>

View File

@ -6,61 +6,177 @@
<title>SENTARA Dashboard</title>
@vite(['resources/css/app.css','resources/js/app.js'])
<script src="https://unpkg.com/feather-icons"></script>
</head>
<body class="bg-gray-100">
<!-- WRAPPER -->
<div class="min-h-screen">
<!-- SIDEBAR FIX -->
<aside class="w-64 fixed top-0 left-0 h-screen
<!-- SIDEBAR -->
<aside id="sidebar"
class="fixed top-0 left-0 h-full w-64
bg-gradient-to-b from-blue-900 to-blue-700
text-white flex flex-col">
text-white flex flex-col
transform -translate-x-full md:translate-x-0
transition duration-300 z-50">
<!-- Logo -->
<div class="px-6 py-6 text-2xl font-bold tracking-wide">
SENTARA
<!-- LOGO -->
<div class="flex justify-center mt-6 mb-8">
<img src="{{ asset('images/tulisan-sentara.png') }}"
class="w-44">
</div>
<!-- Menu -->
<nav class="flex-1 px-4 space-y-2 text-sm font-medium">
<a href="{{ route('dashboard') }}" class="block px-4 py-2 rounded-lg hover:bg-blue-800">
Dashboard Sentimen
<!-- MENU -->
<nav class="flex-1 px-6 space-y-3 text-sm font-medium">
<!-- DASHBOARD -->
<a href="{{ route('dashboard') }}"
class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition">
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-lg">
<i data-feather="grid" class="w-5 h-5"></i>
</div>
<span class="text-[15px]">Dashboard Sentimen</span>
</a>
<a href="{{ route('ulasan.index') }}" class="block px-4 py-2 rounded-lg hover:bg-blue-800">
Data Ulasan
<!-- DATA ULASAN -->
<a href="{{ route('ulasan.index') }}"
class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition">
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-lg">
<i data-feather="file-text" class="w-5 h-5"></i>
</div>
<span class="text-[15px]">Data Ulasan</span>
</a>
<!-- <a href="{{ route('analisis.index') }}"
class="block px-4 py-2 rounded-lg hover:bg-blue-800">
Hasil Analisis Sentimen
</a> -->
<!-- <a href="{{ route('rekomendasi.index') }}"
class="block px-4 py-2 rounded-lg hover:bg-blue-800">
Rekomendasi Layanan
</a> -->
</nav>
<!-- Logout -->
<!-- USER SECTION -->
<div class="px-6 pb-6">
<form method="POST" action="{{ route('logout') }}"
onsubmit="return confirm('Apakah Anda yakin ingin keluar?')">
<div class="relative">
<!-- USER BUTTON -->
<button onclick="toggleUserMenu()"
class="w-full flex items-center gap-3 bg-blue-800 hover:bg-blue-900 px-4 py-3 rounded-xl transition">
<div class="w-10 h-10 flex items-center justify-center bg-blue-700 rounded-full">
<i data-feather="user" class="w-5 h-5"></i>
</div>
<div class="text-left">
<p class="text-sm font-semibold">
{{ auth()->user()->name }}
</p>
<p class="text-xs text-blue-200">Staff</p>
</div>
</button>
<!-- DROPDOWN -->
<div id="userMenu"
class="hidden absolute bottom-16 left-0 w-full
bg-white text-gray-700 rounded-xl shadow-lg overflow-hidden">
<!-- PROFILE -->
<a href="{{ route('profile') }}"
class="flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-100">
<i data-feather="user" class="w-4 h-4"></i>
Profile
</a>
<!-- LOGOUT -->
<form id="logoutForm" method="POST" action="{{ route('logout') }}">
@csrf
<button class="w-full bg-blue-800 hover:bg-blue-900 px-4 py-2 rounded-lg">
<button type="button"
onclick="confirmLogout()"
class="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-100">
<i data-feather="log-out" class="w-4 h-4"></i>
Logout
</button>
</form>
</div>
</div>
</div>
</aside>
<!-- CONTENT FIX -->
<div class="ml-64 min-h-screen p-10">
<!-- CONTENT -->
<div class="flex-1 p-4 md:p-6 md:ml-64 ml-0">
<button onclick="toggleSidebar()"
class="md:hidden mb-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow">
</button>
@yield('content')
</div>
</div>
<!-- MODAL LOGOUT -->
<div id="logoutModal"
class="hidden fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
<div class="bg-white rounded-xl p-6 w-80 text-center shadow-lg">
<h3 class="text-lg font-semibold mb-2">
Konfirmasi Logout
</h3>
<p class="text-sm text-gray-600 mb-4">
Apakah Anda ingin keluar?
</p>
<div class="flex justify-center gap-3">
<button onclick="closeModal()"
class="px-4 py-2 bg-gray-200 rounded-lg">
Tidak
</button>
<button onclick="submitLogout()"
class="px-4 py-2 bg-red-500 text-white rounded-lg">
Ya
</button>
</div>
</div>
</div>
<!-- SCRIPT -->
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('-translate-x-full');
}
function toggleUserMenu() {
document.getElementById('userMenu').classList.toggle('hidden');
}
function confirmLogout(){
document.getElementById('logoutModal').classList.remove('hidden');
}
function closeModal(){
document.getElementById('logoutModal').classList.add('hidden');
}
function submitLogout(){
document.getElementById('logoutForm').submit();
}
feather.replace();
</script>
</body>
</html>

View File

@ -1,29 +0,0 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Profile') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-profile-information-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-password-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.delete-user-form')
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,125 @@
@extends('layouts.sentara')
@section('content')
<div class="min-h-screen bg-[#f5f7fb] px-6 py-8">
<!-- HEADER -->
<div class="flex items-center gap-4 mb-6">
<div>
<h1 class="text-3xl font-bold text-gray-800">Profile Saya</h1>
<p class="text-gray-500 text-sm">
Informasi akun pengguna <span class="text-blue-600 font-medium">SENTARA</span>
</p>
</div>
</div>
<!-- PROFILE CARD -->
<div class="bg-white rounded-2xl shadow-md p-6 flex items-center gap-6 mb-8 relative overflow-hidden">
<div class="absolute right-0 bottom-0 w-64 h-64 bg-blue-100 opacity-30 rounded-full"></div>
<div class="w-24 h-24 rounded-full bg-gradient-to-r from-blue-500 to-blue-700
flex items-center justify-center text-white text-3xl font-bold shadow-md">
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
</div>
<div>
<h2 class="text-2xl font-semibold text-gray-800">
{{ Auth::user()->name }}
</h2>
<p class="text-gray-500 mt-1">
Staff - Dinas Pariwisata Jember
</p>
<span class="inline-block mt-3 text-xs bg-blue-100 text-blue-600 px-3 py-1 rounded-full font-medium">
STAFF
</span>
</div>
</div>
<!-- INFORMASI AKUN -->
<div class="mb-4 flex items-center gap-2 text-blue-600 font-semibold">
<span class="text-xl">📋</span> Informasi Akun
</div>
<!-- GRID -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- NAMA -->
<div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition">
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center text-blue-600">
👤
</div>
<p class="text-gray-400 text-sm">Nama Lengkap</p>
<p class="font-semibold text-gray-800 mt-1">
{{ Auth::user()->name }}
</p>
</div>
<!-- EMAIL -->
<div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition">
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center text-blue-600">
✉️
</div>
<p class="text-gray-400 text-sm">Email</p>
<p class="font-semibold text-gray-800 mt-1">
{{ Auth::user()->email }}
</p>
</div>
<!-- ROLE (FIX ICON) -->
<div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition">
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center text-blue-600">
<!-- ICON GROUP -->
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 20h5v-1a4 4 0 00-3-3.87M9 20H4v-1a4 4 0 013-3.87m10-2.13a4 4 0 10-8 0m8 0a4 4 0 01-8 0"/>
</svg>
</div>
<p class="text-gray-400 text-sm">Role</p>
<p class="font-semibold text-gray-800 mt-1">Staff</p>
</div>
<!-- STATUS -->
<div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition">
<div class="w-12 h-12 mx-auto mb-3 bg-green-100 rounded-full flex items-center justify-center text-green-600">
</div>
<p class="text-gray-400 text-sm">Status Akun</p>
<span class="inline-block mt-1 text-sm bg-green-100 text-green-600 px-3 py-1 rounded-full">
Aktif
</span>
</div>
</div>
<!-- INFO SISTEM -->
<div class="bg-white rounded-xl shadow-sm p-6 flex items-center justify-between">
<div>
<div class="flex items-center gap-2 text-blue-600 font-semibold mb-2">
<span class="text-xl"></span> Informasi Sistem
</div>
<p class="text-gray-500 text-sm max-w-xl">
Akun ini digunakan untuk mengakses sistem SENTARA
(Sistem Analisis Sentimen Ulasan Dinas Pariwisata Jember).
</p>
</div>
</div>
<!-- FOOTER -->
<div class="flex justify-between text-sm text-gray-400 mt-6">
<span>© 2025 SENTARA. All rights reserved.</span>
<span>Dinas Pariwisata Jember</span>
</div>
</div>
@endsection

View File

@ -1,55 +0,0 @@
<section class="space-y-6">
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Delete Account') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
</p>
</header>
<x-danger-button
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
>{{ __('Delete Account') }}</x-danger-button>
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
@csrf
@method('delete')
<h2 class="text-lg font-medium text-gray-900">
{{ __('Are you sure you want to delete your account?') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</p>
<div class="mt-6">
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}"
/>
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-danger-button class="ms-3">
{{ __('Delete Account') }}
</x-danger-button>
</div>
</form>
</x-modal>
</section>

View File

@ -1,48 +0,0 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Update Password') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Ensure your account is using a long, random password to stay secure.') }}
</p>
</header>
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
@csrf
@method('put')
<div>
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password" :value="__('New Password')" />
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'password-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@ -1,64 +0,0 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Profile Information') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __("Update your account's profile information and email address.") }}
</p>
</header>
<form id="send-verification" method="post" action="{{ route('verification.send') }}">
@csrf
</form>
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
@csrf
@method('patch')
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
<x-input-error class="mt-2" :messages="$errors->get('name')" />
</div>
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" />
<x-input-error class="mt-2" :messages="$errors->get('email')" />
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
<div>
<p class="text-sm mt-2 text-gray-800">
{{ __('Your email address is unverified.') }}
<button form="send-verification" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
@if (session('status') === 'verification-link-sent')
<p class="mt-2 font-medium text-sm text-green-600">
{{ __('A new verification link has been sent to your email address.') }}
</p>
@endif
</div>
@endif
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'profile-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@ -2,7 +2,7 @@
@section('content')
<div class="max-w-7xl mx-auto space-y-6">
<div class="max-w-7xl mx-auto px-4 md:px-0 space-y-6">
<!-- HEADER -->
<h2 class="text-2xl font-bold text-gray-800">Data Ulasan</h2>
@ -12,7 +12,7 @@
<!-- ============================= -->
<div class="bg-white rounded-xl shadow p-6">
<div class="grid grid-cols-3 gap-6 items-center">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 items-center">
<!-- UPLOAD -->
<div class="border-2 border-dashed border-blue-300 rounded-xl p-6 text-center">
@ -109,7 +109,7 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm sha
<!-- ============================= -->
<!-- 🔍 SEARCH + FILTER -->
<!-- ============================= -->
<form method="GET" action="{{ route('ulasan.index') }}" class="flex gap-3">
<form method="GET" action="{{ route('ulasan.index') }}" class="flex flex-col md:flex-row gap-3">
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Cari ulasan atau wisata..."
@ -140,6 +140,7 @@ class="bg-gray-200 px-4 py-2 rounded-lg text-sm">
<!-- TABLE -->
<!-- ============================= -->
<div class="bg-white rounded-xl shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-100">
@ -190,7 +191,9 @@ class="bg-gray-200 px-4 py-2 rounded-lg text-sm">
</tbody>
</table>
<div class="p-4 flex justify-between items-center text-sm text-gray-500">
</div>
<div class="p-4 flex flex-col md:flex-row gap-3 justify-between items-center text-sm text-gray-500">
<!-- KIRI (FINAL FIX) -->
<div>

View File

@ -25,9 +25,11 @@
// })->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::get('/profile', function () {
return view('profile.index');
})->name('profile');
// Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
// Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::get('/ulasan', [UlasanController::class, 'index'])->name('ulasan.index');
Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData'])->name('ulasan.ambil');
Route::post('/ulasan/analisis', [UlasanController::class, 'analisisData'])->name('ulasan.analisis');

142
scraper/analisis.py Normal file
View File

@ -0,0 +1,142 @@
import re
import pandas as pd
import pymysql
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import ComplementNB
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
from Sastrawi.StopWordRemover.StopWordRemoverFactory import StopWordRemoverFactory
# ===============================
# 🔌 KONEKSI DB
# ===============================
conn = pymysql.connect(
host='localhost',
user='root',
password='',
database='sentara',
cursorclass=pymysql.cursors.DictCursor
)
cursor = conn.cursor()
# ===============================
# 1. AMBIL DATA
# ===============================
df = pd.read_sql("SELECT id, wisata, ulasan FROM ulasan", conn)
# ===============================
# 2. CLEANING
# ===============================
df = df.dropna(subset=["ulasan"])
df["ulasan"] = df["ulasan"].astype(str)
df = df[df["ulasan"].str.strip() != ""]
# ===============================
# 3. PREPROCESSING
# ===============================
stemmer = StemmerFactory().create_stemmer()
stop_factory = StopWordRemoverFactory()
stopwords = set(stop_factory.get_stop_words())
def clean_text(text):
text = text.lower()
text = re.sub(r"http\S+", " ", text)
text = re.sub(r"[^a-zA-Z\s]", " ", text)
text = re.sub(r"\s+", " ", text)
words = [w for w in text.split() if w not in stopwords]
return stemmer.stem(" ".join(words))
df["clean"] = df["ulasan"].apply(clean_text)
# ===============================
# 4. LABEL SEMENTARA (dummy)
# ===============================
def label_rule(text):
if "tidak" in text or "buruk" in text:
return "negatif"
elif "bagus" in text or "indah" in text:
return "positif"
else:
return "netral"
df["label"] = df["clean"].apply(label_rule)
# ===============================
# 5. TF-IDF + MODEL
# ===============================
X = df["clean"]
y = df["label"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
vectorizer = TfidfVectorizer()
X_train_vec = vectorizer.fit_transform(X_train)
X_test_vec = vectorizer.transform(X_test)
model = ComplementNB()
model.fit(X_train_vec, y_train)
# ===============================
# 6. EVALUASI
# ===============================
y_pred = model.predict(X_test_vec)
acc = accuracy_score(y_test, y_pred)
report = classification_report(y_test, y_pred, output_dict=True)
cm = confusion_matrix(y_test, y_pred, labels=["negatif","netral","positif"])
# ===============================
# 7. PREDIKSI SEMUA DATA
# ===============================
X_all = vectorizer.transform(df["clean"])
df["prediksi"] = model.predict(X_all)
# ===============================
# 8. SIMPAN KE DB
# ===============================
# kosongkan tabel
cursor.execute("DELETE FROM hasil_analisis")
cursor.execute("DELETE FROM evaluasi_model")
# simpan hasil per ulasan
for _, row in df.iterrows():
cursor.execute("""
INSERT INTO hasil_analisis
(wisata, ulasan_terolah, hasil_preprocessing, sentimen, probabilitas, created_at)
VALUES (%s,%s,%s,%s,%s,NOW())
""", (
row["wisata"],
row["ulasan"],
row["clean"],
row["prediksi"],
0.9
))
# simpan evaluasi
cursor.execute("""
INSERT INTO evaluasi_model
(precision, recall, f1_score, accuracy, tp, tn, fp, fn, created_at)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,NOW())
""", (
report["weighted avg"]["precision"],
report["weighted avg"]["recall"],
report["weighted avg"]["f1-score"],
acc,
int(cm[2][2]),
int(cm[0][0]),
int(cm[0][2]),
int(cm[2][0])
))
conn.commit()
conn.close()
print("Analisis selesai ✔")