init commit
This commit is contained in:
commit
b2e1785ad1
|
|
@ -0,0 +1,18 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[compose.yaml]
|
||||||
|
indent_size = 4
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Laravel
|
||||||
|
|
||||||
|
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||||
|
|
||||||
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||||
|
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||||
|
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||||
|
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||||
|
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||||
|
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||||
|
|
||||||
|
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||||
|
|
||||||
|
## Learning Laravel
|
||||||
|
|
||||||
|
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
||||||
|
|
||||||
|
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||||
|
|
||||||
|
## Laravel Sponsors
|
||||||
|
|
||||||
|
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||||
|
|
||||||
|
### Premium Partners
|
||||||
|
|
||||||
|
- **[Vehikl](https://vehikl.com)**
|
||||||
|
- **[Tighten Co.](https://tighten.co)**
|
||||||
|
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||||
|
- **[64 Robots](https://64robots.com)**
|
||||||
|
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||||
|
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||||
|
- **[Redberry](https://redberry.international/laravel-development)**
|
||||||
|
- **[Active Logic](https://activelogic.com)**
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Auth\LoginRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AuthenticatedSessionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the login view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming authentication request.
|
||||||
|
*/
|
||||||
|
public function store(LoginRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->authenticate();
|
||||||
|
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy an authenticated session.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ConfirmablePasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the confirm password view.
|
||||||
|
*/
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
return view('auth.confirm-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm the user's password.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if (! Auth::guard('web')->validate([
|
||||||
|
'email' => $request->user()->email,
|
||||||
|
'password' => $request->password,
|
||||||
|
])) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => __('auth.password'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->put('auth.password_confirmed_at', time());
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EmailVerificationNotificationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send a new email verification notification.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->sendEmailVerificationNotification();
|
||||||
|
|
||||||
|
return back()->with('status', 'verification-link-sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class EmailVerificationPromptController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the email verification prompt.
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request): RedirectResponse|View
|
||||||
|
{
|
||||||
|
return $request->user()->hasVerifiedEmail()
|
||||||
|
? redirect()->intended(route('dashboard', absolute: false))
|
||||||
|
: view('auth.verify-email');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class NewPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset view.
|
||||||
|
*/
|
||||||
|
public function create(Request $request): View
|
||||||
|
{
|
||||||
|
return view('auth.reset-password', ['request' => $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming new password request.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'token' => ['required'],
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Here we will attempt to reset the user's password. If it is successful we
|
||||||
|
// will update the password on an actual user model and persist it to the
|
||||||
|
// database. Otherwise we will parse the error and return the response.
|
||||||
|
$status = Password::reset(
|
||||||
|
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||||
|
function (User $user) use ($request) {
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'remember_token' => Str::random(60),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
event(new PasswordReset($user));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the password was successfully reset, we will redirect the user back to
|
||||||
|
// the application's home authenticated view. If there is an error we can
|
||||||
|
// redirect them back to where they came from with their error message.
|
||||||
|
return $status == Password::PASSWORD_RESET
|
||||||
|
? redirect()->route('login')->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class PasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the user's password.
|
||||||
|
*/
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validateWithBag('updatePassword', [
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('status', 'password-updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class PasswordResetLinkController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset link request view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.forgot-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming password reset link request.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// We will send the password reset link to this user. Once we have attempted
|
||||||
|
// to send the link, we will examine the response then see the message we
|
||||||
|
// need to show to the user. Finally, we'll send out a proper response.
|
||||||
|
$status = Password::sendResetLink(
|
||||||
|
$request->only('email')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $status == Password::RESET_LINK_SENT
|
||||||
|
? back()->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\Registered;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class RegisteredUserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the registration view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.register');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming registration request.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new Registered($user));
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
|
||||||
|
return redirect(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Auth\Events\Verified;
|
||||||
|
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class VerifyEmailController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mark the authenticated user's email address as verified.
|
||||||
|
*/
|
||||||
|
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
|
event(new Verified($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,731 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Review;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* HALAMAN DASHBOARD
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$data = $this->getCommonData($request);
|
||||||
|
return view('dashboard', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* HALAMAN DATA MANAGEMENT
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function dataManagement(Request $request)
|
||||||
|
{
|
||||||
|
$data = $this->getCommonData($request);
|
||||||
|
|
||||||
|
// Tambahkan pagination untuk data management
|
||||||
|
$perPage = $request->get('per_page', 10);
|
||||||
|
$search = $request->get('search', '');
|
||||||
|
|
||||||
|
$query = Review::query();
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$query->where(function($q) use ($search) {
|
||||||
|
$q->where('review', 'like', "%{$search}%")
|
||||||
|
->orWhere('text_final', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter berdasarkan sentimen
|
||||||
|
if ($request->has('filter_sentiment') && $request->filter_sentiment != '') {
|
||||||
|
$query->where('sentiment', $request->filter_sentiment);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['reviews'] = $query->latest()->paginate($perPage);
|
||||||
|
$data['search'] = $search;
|
||||||
|
$data['filter_sentiment'] = $request->filter_sentiment;
|
||||||
|
$data['per_page'] = $perPage;
|
||||||
|
|
||||||
|
return view('data-management', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* FUNGSI AMBIL DATA UMUM
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
private function getCommonData($request = null)
|
||||||
|
{
|
||||||
|
// Cek apakah tabel reviews ada
|
||||||
|
if (!Schema::hasTable('reviews')) {
|
||||||
|
return $this->getEmptyData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter berdasarkan tanggal jika ada
|
||||||
|
$query = Review::query();
|
||||||
|
|
||||||
|
if ($request && $request->has('start_date') && $request->has('end_date') && $request->start_date && $request->end_date) {
|
||||||
|
$query->whereBetween('created_at', [
|
||||||
|
$request->start_date . ' 00:00:00',
|
||||||
|
$request->end_date . ' 23:59:59'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter berdasarkan sentimen
|
||||||
|
if ($request && $request->has('sentiment') && $request->sentiment != '') {
|
||||||
|
$query->where('sentiment', $request->sentiment);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataset = $query->latest()->get();
|
||||||
|
|
||||||
|
// Hitung sentimen
|
||||||
|
$positif = (clone $query)->where('sentiment', 'positif')->count();
|
||||||
|
$negatif = (clone $query)->where('sentiment', 'negatif')->count();
|
||||||
|
|
||||||
|
$totalData = $positif + $negatif;
|
||||||
|
$totalSafe = $totalData > 0 ? $totalData : 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* SCORE DISTRIBUTION UNTUK CHART (5 Range)
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
$scoreDistribution = [
|
||||||
|
'Very Negative (-5 to -9)' => 0,
|
||||||
|
'Negative (-1 to -4)' => 0,
|
||||||
|
'Positive (1 to 4)' => 0,
|
||||||
|
'Very Positive (5 to 9)' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Untuk chart line distribusi (0-100)
|
||||||
|
$scoreChartDistribution = [
|
||||||
|
'0-20' => 0,
|
||||||
|
'21-40' => 0,
|
||||||
|
'41-60' => 0,
|
||||||
|
'61-80' => 0,
|
||||||
|
'81-100' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* MAINTENANCE CATEGORY
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
$maintenance = [
|
||||||
|
'Login / Akses' => 0,
|
||||||
|
'Performa Sistem (Server/Lambat)' => 0,
|
||||||
|
'Fitur Pembelajaran' => 0,
|
||||||
|
'UI / Tampilan' => 0,
|
||||||
|
'Bug / Error' => 0,
|
||||||
|
'Aplikasi Mobile' => 0,
|
||||||
|
'Ujian / Exam' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$maintenanceDetails = [
|
||||||
|
'Login / Akses' => [],
|
||||||
|
'Performa Sistem (Server/Lambat)' => [],
|
||||||
|
'Fitur Pembelajaran' => [],
|
||||||
|
'UI / Tampilan' => [],
|
||||||
|
'Bug / Error' => [],
|
||||||
|
'Aplikasi Mobile' => [],
|
||||||
|
'Ujian / Exam' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($dataset as $row) {
|
||||||
|
$review = strtolower($row->review ?? '');
|
||||||
|
$text = strtolower($row->steming_data ?? '');
|
||||||
|
$score = (int)($row->score ?? 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SCORE DISTRIBUTION
|
||||||
|
*/
|
||||||
|
if ($score <= -5) {
|
||||||
|
$scoreDistribution['Very Negative (-5 to -9)']++;
|
||||||
|
} elseif ($score <= -1) {
|
||||||
|
$scoreDistribution['Negative (-1 to -4)']++;
|
||||||
|
} elseif ($score >= 5) {
|
||||||
|
$scoreDistribution['Very Positive (5 to 9)']++;
|
||||||
|
} elseif ($score >= 1) {
|
||||||
|
$scoreDistribution['Positive (1 to 4)']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SCORE DISTRIBUTION UNTUK CHART LINE (0-100)
|
||||||
|
* Konversi score dari range -9..9 ke 0..100
|
||||||
|
*/
|
||||||
|
$normalizedScore = (($score + 9) / 18) * 100;
|
||||||
|
|
||||||
|
if ($normalizedScore <= 20) {
|
||||||
|
$scoreChartDistribution['0-20']++;
|
||||||
|
} elseif ($normalizedScore <= 40) {
|
||||||
|
$scoreChartDistribution['21-40']++;
|
||||||
|
} elseif ($normalizedScore <= 60) {
|
||||||
|
$scoreChartDistribution['41-60']++;
|
||||||
|
} elseif ($normalizedScore <= 80) {
|
||||||
|
$scoreChartDistribution['61-80']++;
|
||||||
|
} else {
|
||||||
|
$scoreChartDistribution['81-100']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAINTENANCE DETECTION
|
||||||
|
*/
|
||||||
|
$combinedText = $text . ' ' . $review;
|
||||||
|
|
||||||
|
$keywords = [
|
||||||
|
'Login / Akses' => ['login', 'akses', 'masuk', 'akun', 'log in', 'sign in'],
|
||||||
|
'Performa Sistem (Server/Lambat)' => ['lambat', 'server', 'loading', 'lemot', 'slow', 'cepat', 'lancar', 'responsif'],
|
||||||
|
'Fitur Pembelajaran' => ['materi', 'tugas', 'belajar', 'pembelajaran', 'course', 'modul', 'video', 'konten'],
|
||||||
|
'UI / Tampilan' => ['tampilan', 'ui', 'desain', 'interface', 'ux', 'user interface', 'user experience'],
|
||||||
|
'Bug / Error' => ['error', 'bug', 'crash', 'force close', 'warning', 'galat', 'masalah'],
|
||||||
|
'Aplikasi Mobile' => ['mobile', 'android', 'ios', 'hp', 'handphone', 'app', 'aplikasi'],
|
||||||
|
'Ujian / Exam' => ['ujian', 'exam', 'test', 'quiz', 'nilai', 'skor', 'ujian online']
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($keywords as $category => $words) {
|
||||||
|
foreach ($words as $word) {
|
||||||
|
if (str_contains($combinedText, $word)) {
|
||||||
|
$maintenance[$category]++;
|
||||||
|
if (count($maintenanceDetails[$category]) < 10) {
|
||||||
|
$maintenanceDetails[$category][] = $row->review;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CONFUSION MATRIX
|
||||||
|
*/
|
||||||
|
$cmPath = storage_path('app/public/confusion_matrix.csv');
|
||||||
|
$cm = file_exists($cmPath) ? $this->readCSV($cmPath) : [
|
||||||
|
['', 'Pred Negatif', 'Pred Positif'],
|
||||||
|
['Actual Negatif', 0, 0],
|
||||||
|
['Actual Positif', 0, 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* METRICS - Disesuaikan dengan format dari Colab
|
||||||
|
*/
|
||||||
|
$metricsPath = storage_path('app/public/evaluation_metrics.csv');
|
||||||
|
$metrics = file_exists($metricsPath) ? $this->readMetrics($metricsPath) : $this->getDefaultMetrics();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FORMAT METRICS UNTUK CHART
|
||||||
|
*/
|
||||||
|
$formattedMetrics = $this->formatMetricsForChart($metrics);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'dataset' => $dataset,
|
||||||
|
'positif' => $positif,
|
||||||
|
'negatif' => $negatif,
|
||||||
|
'totalData' => $totalData,
|
||||||
|
'totalSafe' => $totalSafe,
|
||||||
|
'maintenance' => $maintenance,
|
||||||
|
'maintenanceDetails' => $maintenanceDetails,
|
||||||
|
'scoreDistribution' => $scoreDistribution,
|
||||||
|
'scoreChartDistribution' => $scoreChartDistribution,
|
||||||
|
'score_0_20' => $scoreChartDistribution['0-20'],
|
||||||
|
'score_21_40' => $scoreChartDistribution['21-40'],
|
||||||
|
'score_41_60' => $scoreChartDistribution['41-60'],
|
||||||
|
'score_61_80' => $scoreChartDistribution['61-80'],
|
||||||
|
'score_81_100' => $scoreChartDistribution['81-100'],
|
||||||
|
'cm' => $cm,
|
||||||
|
'metrics' => $metrics,
|
||||||
|
'formattedMetrics' => $formattedMetrics
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format metrics untuk chart
|
||||||
|
*/
|
||||||
|
private function formatMetricsForChart($metrics)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'accuracy' => $metrics['accuracy'] ?? 0,
|
||||||
|
'negatif' => [
|
||||||
|
'precision' => $metrics['precision_negatif'] ?? 0,
|
||||||
|
'recall' => $metrics['recall_negatif'] ?? 0,
|
||||||
|
'f1' => $metrics['f1_negatif'] ?? 0
|
||||||
|
],
|
||||||
|
'positif' => [
|
||||||
|
'precision' => $metrics['precision_positif'] ?? 0,
|
||||||
|
'recall' => $metrics['recall_positif'] ?? 0,
|
||||||
|
'f1' => $metrics['f1_positif'] ?? 0
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default metrics structure
|
||||||
|
*/
|
||||||
|
private function getDefaultMetrics()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'accuracy' => 0,
|
||||||
|
'precision_negatif' => 0,
|
||||||
|
'precision_positif' => 0,
|
||||||
|
'recall_negatif' => 0,
|
||||||
|
'recall_positif' => 0,
|
||||||
|
'f1_negatif' => 0,
|
||||||
|
'f1_positif' => 0,
|
||||||
|
'macro_avg_precision' => 0,
|
||||||
|
'macro_avg_recall' => 0,
|
||||||
|
'macro_avg_f1' => 0,
|
||||||
|
'weighted_avg_precision' => 0,
|
||||||
|
'weighted_avg_recall' => 0,
|
||||||
|
'weighted_avg_f1' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get empty data structure
|
||||||
|
*/
|
||||||
|
private function getEmptyData()
|
||||||
|
{
|
||||||
|
$maintenance = [
|
||||||
|
'Login / Akses' => 0,
|
||||||
|
'Performa Sistem (Server/Lambat)' => 0,
|
||||||
|
'Fitur Pembelajaran' => 0,
|
||||||
|
'UI / Tampilan' => 0,
|
||||||
|
'Bug / Error' => 0,
|
||||||
|
'Aplikasi Mobile' => 0,
|
||||||
|
'Ujian / Exam' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$maintenanceDetails = [
|
||||||
|
'Login / Akses' => [],
|
||||||
|
'Performa Sistem (Server/Lambat)' => [],
|
||||||
|
'Fitur Pembelajaran' => [],
|
||||||
|
'UI / Tampilan' => [],
|
||||||
|
'Bug / Error' => [],
|
||||||
|
'Aplikasi Mobile' => [],
|
||||||
|
'Ujian / Exam' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$scoreDistribution = [
|
||||||
|
'Very Negative (-5 to -9)' => 0,
|
||||||
|
'Negative (-1 to -4)' => 0,
|
||||||
|
'Positive (1 to 4)' => 0,
|
||||||
|
'Very Positive (5 to 9)' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$scoreChartDistribution = [
|
||||||
|
'0-20' => 0,
|
||||||
|
'21-40' => 0,
|
||||||
|
'41-60' => 0,
|
||||||
|
'61-80' => 0,
|
||||||
|
'81-100' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'dataset' => collect([]),
|
||||||
|
'positif' => 0,
|
||||||
|
'negatif' => 0,
|
||||||
|
'totalData' => 0,
|
||||||
|
'totalSafe' => 1,
|
||||||
|
'maintenance' => $maintenance,
|
||||||
|
'maintenanceDetails' => $maintenanceDetails,
|
||||||
|
'scoreDistribution' => $scoreDistribution,
|
||||||
|
'scoreChartDistribution' => $scoreChartDistribution,
|
||||||
|
'score_0_20' => 0,
|
||||||
|
'score_21_40' => 0,
|
||||||
|
'score_41_60' => 0,
|
||||||
|
'score_61_80' => 0,
|
||||||
|
'score_81_100' => 0,
|
||||||
|
'cm' => [
|
||||||
|
['', 'Pred Negatif', 'Pred Positif'],
|
||||||
|
['Actual Negatif', 0, 0],
|
||||||
|
['Actual Positif', 0, 0]
|
||||||
|
],
|
||||||
|
'metrics' => $this->getDefaultMetrics(),
|
||||||
|
'formattedMetrics' => $this->formatMetricsForChart($this->getDefaultMetrics())
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* READ METRICS DARI CSV (disesuaikan dengan format dari Colab)
|
||||||
|
*/
|
||||||
|
private function readMetrics($path)
|
||||||
|
{
|
||||||
|
$default = $this->getDefaultMetrics();
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->readCSV($path);
|
||||||
|
$metrics = $default;
|
||||||
|
|
||||||
|
foreach ($data as $row) {
|
||||||
|
if (isset($row[0]) && isset($row[1])) {
|
||||||
|
$key = strtolower(trim($row[0]));
|
||||||
|
$value = (float) trim($row[1]);
|
||||||
|
|
||||||
|
// Mapping key dari CSV ke array metrics
|
||||||
|
if (str_contains($key, 'accuracy')) {
|
||||||
|
$metrics['accuracy'] = $value;
|
||||||
|
} elseif (str_contains($key, 'precision_negatif') || str_contains($key, 'precision negatif')) {
|
||||||
|
$metrics['precision_negatif'] = $value;
|
||||||
|
} elseif (str_contains($key, 'precision_positif') || str_contains($key, 'precision positif')) {
|
||||||
|
$metrics['precision_positif'] = $value;
|
||||||
|
} elseif (str_contains($key, 'recall_negatif') || str_contains($key, 'recall negatif')) {
|
||||||
|
$metrics['recall_negatif'] = $value;
|
||||||
|
} elseif (str_contains($key, 'recall_positif') || str_contains($key, 'recall positif')) {
|
||||||
|
$metrics['recall_positif'] = $value;
|
||||||
|
} elseif (str_contains($key, 'f1_negatif') || str_contains($key, 'f1 negatif') || str_contains($key, 'f1-score negatif')) {
|
||||||
|
$metrics['f1_negatif'] = $value;
|
||||||
|
} elseif (str_contains($key, 'f1_positif') || str_contains($key, 'f1 positif') || str_contains($key, 'f1-score positif')) {
|
||||||
|
$metrics['f1_positif'] = $value;
|
||||||
|
} elseif (str_contains($key, 'macro_avg_precision') || str_contains($key, 'macro avg precision')) {
|
||||||
|
$metrics['macro_avg_precision'] = $value;
|
||||||
|
} elseif (str_contains($key, 'macro_avg_recall') || str_contains($key, 'macro avg recall')) {
|
||||||
|
$metrics['macro_avg_recall'] = $value;
|
||||||
|
} elseif (str_contains($key, 'macro_avg_f1') || str_contains($key, 'macro avg f1')) {
|
||||||
|
$metrics['macro_avg_f1'] = $value;
|
||||||
|
} elseif (str_contains($key, 'weighted_avg_precision') || str_contains($key, 'weighted avg precision')) {
|
||||||
|
$metrics['weighted_avg_precision'] = $value;
|
||||||
|
} elseif (str_contains($key, 'weighted_avg_recall') || str_contains($key, 'weighted avg recall')) {
|
||||||
|
$metrics['weighted_avg_recall'] = $value;
|
||||||
|
} elseif (str_contains($key, 'weighted_avg_f1') || str_contains($key, 'weighted avg f1')) {
|
||||||
|
$metrics['weighted_avg_f1'] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* UPLOAD CSV → INSERT DATABASE
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function upload(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'dataset' => 'required|mimes:csv,txt|max:10240',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return back()->withErrors($validator)->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('dataset');
|
||||||
|
$path = $file->getRealPath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rows = $this->readCSV($path);
|
||||||
|
|
||||||
|
if (count($rows) <= 1) {
|
||||||
|
return back()->with('error', 'File CSV tidak memiliki data!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = array_shift($rows); // Hapus header
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
if ($request->has('replace_data') && $request->replace_data == '1') {
|
||||||
|
Review::truncate();
|
||||||
|
}
|
||||||
|
|
||||||
|
$inserted = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
try {
|
||||||
|
// Validasi minimal memiliki 4 kolom
|
||||||
|
if (count($row) >= 4) {
|
||||||
|
|
||||||
|
// Parse sentiment
|
||||||
|
$sentiment = strtolower(trim($row[3] ?? ''));
|
||||||
|
// Pastikan sentiment hanya positif atau negatif
|
||||||
|
if (!in_array($sentiment, ['positif', 'negatif'])) {
|
||||||
|
$sentiment = $sentiment == 'positive' ? 'positif' : ($sentiment == 'negative' ? 'negatif' : 'netral');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse score
|
||||||
|
$score = is_numeric($row[2] ?? '') ? (int)$row[2] : 0;
|
||||||
|
|
||||||
|
Review::create([
|
||||||
|
'review' => $row[0] ?? '',
|
||||||
|
'steming_data' => $row[1] ?? '',
|
||||||
|
'score' => $score,
|
||||||
|
'sentiment' => $sentiment,
|
||||||
|
'created_at' => $row[4] ?? now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
$inserted++;
|
||||||
|
} else {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$failed++;
|
||||||
|
// \Log::warning('Gagal insert row: ' . json_encode($row) . ' Error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$message = "Berhasil mengupload {$inserted} data";
|
||||||
|
if ($failed > 0) {
|
||||||
|
$message .= ", {$failed} data gagal diproses";
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', $message);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return back()->with('error', 'Gagal upload: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* HAPUS SEMUA DATA
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function truncate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Review::truncate();
|
||||||
|
return back()->with('success', 'Semua data berhasil dihapus!');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return back()->with('error', 'Gagal menghapus data: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* EXPORT PDF
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function exportPDF(Request $request)
|
||||||
|
{
|
||||||
|
$data = $this->getCommonData($request);
|
||||||
|
$data['title'] = 'Laporan Analisis Sentimen';
|
||||||
|
$data['date'] = now()->format('d F Y');
|
||||||
|
// $data['user'] = auth()->user();
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('pdf.report', $data);
|
||||||
|
$pdf->setPaper('A4', 'landscape');
|
||||||
|
|
||||||
|
return $pdf->download('laporan_sentimen_' . date('Y-m-d') . '.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* EXPORT EXCEL (CSV)
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function exportExcel(Request $request)
|
||||||
|
{
|
||||||
|
$data = $this->getCommonData($request);
|
||||||
|
|
||||||
|
$filename = 'data_sentimen_' . date('Y-m-d') . '.csv';
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => 'text/csv',
|
||||||
|
'Content-Disposition' => "attachment; filename=\"$filename\"",
|
||||||
|
];
|
||||||
|
|
||||||
|
$callback = function() use ($data) {
|
||||||
|
$file = fopen('php://output', 'w');
|
||||||
|
|
||||||
|
// Header CSV
|
||||||
|
fputcsv($file, ['ID', 'Review', 'Steming Data', 'Score', 'Sentimen', 'Tanggal']);
|
||||||
|
|
||||||
|
// Data
|
||||||
|
foreach ($data['dataset'] as $review) {
|
||||||
|
fputcsv($file, [
|
||||||
|
$review->id,
|
||||||
|
$review->review,
|
||||||
|
$review->steming_data,
|
||||||
|
$review->score,
|
||||||
|
$review->sentiment,
|
||||||
|
$review->created_at->format('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return response()->stream($callback, 200, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* API ENDPOINT (Untuk AJAX)
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function getStats(Request $request)
|
||||||
|
{
|
||||||
|
$data = $this->getCommonData($request);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'positif' => $data['positif'],
|
||||||
|
'negatif' => $data['negatif'],
|
||||||
|
'total' => $data['totalData'],
|
||||||
|
'positif_percentage' => $data['totalData'] > 0 ? round(($data['positif'] / $data['totalData']) * 100, 1) : 0,
|
||||||
|
'negatif_percentage' => $data['totalData'] > 0 ? round(($data['negatif'] / $data['totalData']) * 100, 1) : 0,
|
||||||
|
'score_distribution' => [
|
||||||
|
'0-20' => $data['score_0_20'] ?? 0,
|
||||||
|
'21-40' => $data['score_21_40'] ?? 0,
|
||||||
|
'41-60' => $data['score_41_60'] ?? 0,
|
||||||
|
'61-80' => $data['score_61_80'] ?? 0,
|
||||||
|
'81-100' => $data['score_81_100'] ?? 0,
|
||||||
|
],
|
||||||
|
'metrics' => [
|
||||||
|
'accuracy' => $data['metrics']['accuracy'] ?? 0,
|
||||||
|
'negatif' => [
|
||||||
|
'precision' => $data['metrics']['precision_negatif'] ?? 0,
|
||||||
|
'recall' => $data['metrics']['recall_negatif'] ?? 0,
|
||||||
|
'f1' => $data['metrics']['f1_negatif'] ?? 0
|
||||||
|
],
|
||||||
|
'positif' => [
|
||||||
|
'precision' => $data['metrics']['precision_positif'] ?? 0,
|
||||||
|
'recall' => $data['metrics']['recall_positif'] ?? 0,
|
||||||
|
'f1' => $data['metrics']['f1_positif'] ?? 0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'maintenance' => $data['maintenance'],
|
||||||
|
'cm' => $data['cm']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* HELPER READ CSV
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
private function readCSV($path)
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
if (!file_exists($path) || !is_readable($path)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = fopen($path, 'r');
|
||||||
|
|
||||||
|
if (!$file) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deteksi delimiter
|
||||||
|
$firstLine = fgets($file);
|
||||||
|
rewind($file);
|
||||||
|
|
||||||
|
$delimiters = [',', ';', "\t", '|'];
|
||||||
|
$delimiter = ',';
|
||||||
|
$maxCount = 0;
|
||||||
|
|
||||||
|
foreach ($delimiters as $d) {
|
||||||
|
$count = count(str_getcsv($firstLine, $d));
|
||||||
|
if ($count > $maxCount) {
|
||||||
|
$maxCount = $count;
|
||||||
|
$delimiter = $d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baca CSV
|
||||||
|
while (($row = fgetcsv($file, 0, $delimiter)) !== FALSE) {
|
||||||
|
$row = array_map('trim', $row);
|
||||||
|
$data[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($file);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* UPDATE SINGLE REVIEW
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function updateReview(Request $request, $id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'review' => 'required|string',
|
||||||
|
'sentiment' => 'required|in:positif,negatif',
|
||||||
|
'score' => 'required|integer|min:-9|max:9'
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$review = Review::findOrFail($id);
|
||||||
|
$review->update([
|
||||||
|
'review' => $request->review,
|
||||||
|
'steming_data' => $request->steming_data ?? $request->review,
|
||||||
|
'score' => $request->score,
|
||||||
|
'sentiment' => $request->sentiment
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Review berhasil diupdate'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Gagal update: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* DELETE SINGLE REVIEW
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function deleteReview($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$review = Review::findOrFail($id);
|
||||||
|
$review->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Review berhasil dihapus'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Gagal hapus: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================
|
||||||
|
* HALAMAN ANALYTICS KHUSUS (Optional)
|
||||||
|
* ===============================
|
||||||
|
*/
|
||||||
|
public function showAnalytics(Request $request)
|
||||||
|
{
|
||||||
|
$data = $this->getCommonData($request);
|
||||||
|
|
||||||
|
// Data tambahan untuk analytics
|
||||||
|
$data['page_title'] = 'Analytics Dashboard';
|
||||||
|
$data['date_range'] = [
|
||||||
|
'start' => $request->start_date ?? now()->subDays(30)->format('Y-m-d'),
|
||||||
|
'end' => $request->end_date ?? now()->format('Y-m-d')
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('analytics', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\ProfileUpdateRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Redirect;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the user's profile form.
|
||||||
|
*/
|
||||||
|
public function edit(Request $request): View
|
||||||
|
{
|
||||||
|
return view('profile.edit', [
|
||||||
|
'user' => $request->user(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile information.
|
||||||
|
*/
|
||||||
|
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->user()->fill($request->validated());
|
||||||
|
|
||||||
|
if ($request->user()->isDirty('email')) {
|
||||||
|
$request->user()->email_verified_at = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->save();
|
||||||
|
|
||||||
|
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the user's account.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validateWithBag('userDeletion', [
|
||||||
|
'password' => ['required', 'current_password'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return Redirect::to('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CheckRole
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next, string $role): Response
|
||||||
|
{
|
||||||
|
// Jika user belum login atau role tidak sesuai, lempar ke dashboard
|
||||||
|
if (!$request->user() || $request->user()->role !== $role) {
|
||||||
|
return redirect()->route('dashboard')->with('error', 'Akses ditolak! Halaman ini hanya untuk Administrator.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class LoginRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => ['required', 'string', 'email'],
|
||||||
|
'password' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate the request's credentials.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function authenticate(): void
|
||||||
|
{
|
||||||
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
|
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||||
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => trans('auth.failed'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RateLimiter::clear($this->throttleKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the login request is not rate limited.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function ensureIsNotRateLimited(): void
|
||||||
|
{
|
||||||
|
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new Lockout($this));
|
||||||
|
|
||||||
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => trans('auth.throttle', [
|
||||||
|
'seconds' => $seconds,
|
||||||
|
'minutes' => ceil($seconds / 60),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limiting throttle key for the request.
|
||||||
|
*/
|
||||||
|
public function throttleKey(): string
|
||||||
|
{
|
||||||
|
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ProfileUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'lowercase',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class)->ignore($this->user()->id),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
// app/Models/Review.php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Review extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'reviews';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'review',
|
||||||
|
'text_final',
|
||||||
|
'score',
|
||||||
|
'sentiment'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'score' => 'integer',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope untuk filter sentimen positif
|
||||||
|
*/
|
||||||
|
public function scopePositif($query)
|
||||||
|
{
|
||||||
|
return $query->where('sentiment', 'positif');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope untuk filter sentimen negatif
|
||||||
|
*/
|
||||||
|
public function scopeNegatif($query)
|
||||||
|
{
|
||||||
|
return $query->where('sentiment', 'negatif');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope untuk filter berdasarkan score
|
||||||
|
*/
|
||||||
|
public function scopeScoreRange($query, $min, $max)
|
||||||
|
{
|
||||||
|
return $query->whereBetween('score', [$min, $max]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sentiment badge class untuk styling
|
||||||
|
*/
|
||||||
|
public function getSentimentBadgeClassAttribute()
|
||||||
|
{
|
||||||
|
return $this->sentiment === 'positif'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sentiment in uppercase
|
||||||
|
*/
|
||||||
|
public function getSentimentUppercaseAttribute()
|
||||||
|
{
|
||||||
|
return strtoupper($this->sentiment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Sentiment extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'review',
|
||||||
|
'sentiment',
|
||||||
|
'score',
|
||||||
|
'predicted',
|
||||||
|
'aspect'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'role',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AppLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.app');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class GuestLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.guest');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
// Mendaftarkan alias middleware untuk pengecekan role
|
||||||
|
$middleware->alias([
|
||||||
|
'role' => \App\Http\Middleware\CheckRole::class,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
|
//
|
||||||
|
})->create();
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/laravel",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": ["laravel", "framework"],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/tinker": "^2.10.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/breeze": "^2.3",
|
||||||
|
"laravel/pail": "^1.2.2",
|
||||||
|
"laravel/pint": "^1.24",
|
||||||
|
"laravel/sail": "^1.41",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"pestphp/pest": "^4.3",
|
||||||
|
"pestphp/pest-plugin-laravel": "^4.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the number of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "octane",
|
||||||
|
| "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'stores' => [
|
||||||
|
'database',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Set some default values. It is possible to add all defines that can be set
|
||||||
|
| in dompdf_config.inc.php. You can also override the entire config file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||||
|
|
||||||
|
'public_path' => null, // Override the public path if needed
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show € and £.
|
||||||
|
*/
|
||||||
|
'convert_entities' => true,
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
/**
|
||||||
|
* The location of the DOMPDF font directory
|
||||||
|
*
|
||||||
|
* The location of the directory where DOMPDF will store fonts and font metrics
|
||||||
|
* Note: This directory must exist and be writable by the webserver process.
|
||||||
|
* *Please note the trailing slash.*
|
||||||
|
*
|
||||||
|
* Notes regarding fonts:
|
||||||
|
* Additional .afm font metrics can be added by executing load_font.php from command line.
|
||||||
|
*
|
||||||
|
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
|
||||||
|
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
|
||||||
|
* increase file size unless font subsetting is enabled. Before embedding a font please
|
||||||
|
* review your rights under the font license.
|
||||||
|
*
|
||||||
|
* Any font specification in the source HTML is translated to the closest font available
|
||||||
|
* in the font directory.
|
||||||
|
*
|
||||||
|
* The pdf standard "Base 14 fonts" are:
|
||||||
|
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
|
||||||
|
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
|
||||||
|
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||||
|
* Symbol, ZapfDingbats.
|
||||||
|
*/
|
||||||
|
'font_dir' => storage_path('fonts'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of the DOMPDF font cache directory
|
||||||
|
*
|
||||||
|
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
||||||
|
* This directory can be the same as DOMPDF_FONT_DIR
|
||||||
|
*
|
||||||
|
* Note: This directory must exist and be writable by the webserver process.
|
||||||
|
*/
|
||||||
|
'font_cache' => storage_path('fonts'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of a temporary directory.
|
||||||
|
*
|
||||||
|
* The directory specified must be writeable by the webserver process.
|
||||||
|
* The temporary directory is required to download remote images and when
|
||||||
|
* using the PDFLib back end.
|
||||||
|
*/
|
||||||
|
'temp_dir' => sys_get_temp_dir(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ==== IMPORTANT ====
|
||||||
|
*
|
||||||
|
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
||||||
|
* files on the webserver. All local files opened by dompdf must be in a
|
||||||
|
* subdirectory of this directory. DO NOT set it to '/' since this could
|
||||||
|
* allow an attacker to use dompdf to read any files on the server. This
|
||||||
|
* should be an absolute path.
|
||||||
|
* This is only checked on command line call by dompdf.php, but not by
|
||||||
|
* direct class use like:
|
||||||
|
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||||
|
*/
|
||||||
|
'chroot' => realpath(base_path()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol whitelist
|
||||||
|
*
|
||||||
|
* Protocols and PHP wrappers allowed in URIs, and the validation rules
|
||||||
|
* that determine if a resouce may be loaded. Full support is not guaranteed
|
||||||
|
* for the protocols/wrappers specified
|
||||||
|
* by this array.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
'allowed_protocols' => [
|
||||||
|
'data://' => ['rules' => []],
|
||||||
|
'file://' => ['rules' => []],
|
||||||
|
'http://' => ['rules' => []],
|
||||||
|
'https://' => ['rules' => []],
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operational artifact (log files, temporary files) path validation
|
||||||
|
*/
|
||||||
|
'artifactPathValidation' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'log_output_file' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable font subsetting or not.
|
||||||
|
*/
|
||||||
|
'enable_font_subsetting' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PDF rendering backend to use
|
||||||
|
*
|
||||||
|
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
||||||
|
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
||||||
|
* fall back on CPDF. 'GD' renders PDFs to graphic files.
|
||||||
|
* {@link * Canvas_Factory} ultimately determines which rendering class to
|
||||||
|
* instantiate based on this setting.
|
||||||
|
*
|
||||||
|
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
||||||
|
* capabilities for dompdf, however additional features (e.g. object,
|
||||||
|
* image and font support, etc.) differ between backends. Please see
|
||||||
|
* {@link PDFLib_Adapter} for more information on the PDFLib backend
|
||||||
|
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
|
||||||
|
* on CPDF. Also see the documentation for each backend at the links
|
||||||
|
* below.
|
||||||
|
*
|
||||||
|
* The GD rendering backend is a little different than PDFLib and
|
||||||
|
* CPDF. Several features of CPDF and PDFLib are not supported or do
|
||||||
|
* not make any sense when creating image files. For example,
|
||||||
|
* multiple pages are not supported, nor are PDF 'objects'. Have a
|
||||||
|
* look at {@link GD_Adapter} for more information. GD support is
|
||||||
|
* experimental, so use it at your own risk.
|
||||||
|
*
|
||||||
|
* @link http://www.pdflib.com
|
||||||
|
* @link http://www.ros.co.nz/pdf
|
||||||
|
* @link http://www.php.net/image
|
||||||
|
*/
|
||||||
|
'pdf_backend' => 'CPDF',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* html target media view which should be rendered into pdf.
|
||||||
|
* List of types and parsing rules for future extensions:
|
||||||
|
* http://www.w3.org/TR/REC-html40/types.html
|
||||||
|
* screen, tty, tv, projection, handheld, print, braille, aural, all
|
||||||
|
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
|
||||||
|
* Note, even though the generated pdf file is intended for print output,
|
||||||
|
* the desired content might be different (e.g. screen or projection view of html file).
|
||||||
|
* Therefore allow specification of content here.
|
||||||
|
*/
|
||||||
|
'default_media_type' => 'screen',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default paper size.
|
||||||
|
*
|
||||||
|
* North America standard is "letter"; other countries generally "a4"
|
||||||
|
*
|
||||||
|
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||||
|
*/
|
||||||
|
'default_paper_size' => 'a4',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default paper orientation.
|
||||||
|
*
|
||||||
|
* The orientation of the page (portrait or landscape).
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'default_paper_orientation' => 'portrait',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default font family
|
||||||
|
*
|
||||||
|
* Used if no suitable fonts can be found. This must exist in the font folder.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'default_font' => 'serif',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image DPI setting
|
||||||
|
*
|
||||||
|
* This setting determines the default DPI setting for images and fonts. The
|
||||||
|
* DPI may be overridden for inline images by explictly setting the
|
||||||
|
* image's width & height style attributes (i.e. if the image's native
|
||||||
|
* width is 600 pixels and you specify the image's width as 72 points,
|
||||||
|
* the image will have a DPI of 600 in the rendered PDF. The DPI of
|
||||||
|
* background images can not be overridden and is controlled entirely
|
||||||
|
* via this parameter.
|
||||||
|
*
|
||||||
|
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
|
||||||
|
* If a size in html is given as px (or without unit as image size),
|
||||||
|
* this tells the corresponding size in pt.
|
||||||
|
* This adjusts the relative sizes to be similar to the rendering of the
|
||||||
|
* html page in a reference browser.
|
||||||
|
*
|
||||||
|
* In pdf, always 1 pt = 1/72 inch
|
||||||
|
*
|
||||||
|
* Rendering resolution of various browsers in px per inch:
|
||||||
|
* Windows Firefox and Internet Explorer:
|
||||||
|
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
|
||||||
|
* Linux Firefox:
|
||||||
|
* about:config *resolution: Default:96
|
||||||
|
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
|
||||||
|
*
|
||||||
|
* Take care about extra font/image zoom factor of browser.
|
||||||
|
*
|
||||||
|
* In images, <img> size in pixel attribute, img css style, are overriding
|
||||||
|
* the real image dimension in px for rendering.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
'dpi' => 96,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable embedded PHP
|
||||||
|
*
|
||||||
|
* If this setting is set to true then DOMPDF will automatically evaluate embedded PHP contained
|
||||||
|
* within <script type="text/php"> ... </script> tags.
|
||||||
|
*
|
||||||
|
* ==== IMPORTANT ==== Enabling this for documents you do not trust (e.g. arbitrary remote html pages)
|
||||||
|
* is a security risk.
|
||||||
|
* Embedded scripts are run with the same level of system access available to dompdf.
|
||||||
|
* Set this option to false (recommended) if you wish to process untrusted documents.
|
||||||
|
* This setting may increase the risk of system exploit.
|
||||||
|
* Do not change this settings without understanding the consequences.
|
||||||
|
* Additional documentation is available on the dompdf wiki at:
|
||||||
|
* https://github.com/dompdf/dompdf/wiki
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_php' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rnable inline JavaScript
|
||||||
|
*
|
||||||
|
* If this setting is set to true then DOMPDF will automatically insert JavaScript code contained
|
||||||
|
* within <script type="text/javascript"> ... </script> tags as written into the PDF.
|
||||||
|
* NOTE: This is PDF-based JavaScript to be executed by the PDF viewer,
|
||||||
|
* not browser-based JavaScript executed by Dompdf.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_javascript' => true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable remote file access
|
||||||
|
*
|
||||||
|
* If this setting is set to true, DOMPDF will access remote sites for
|
||||||
|
* images and CSS files as required.
|
||||||
|
*
|
||||||
|
* ==== IMPORTANT ====
|
||||||
|
* This can be a security risk, in particular in combination with isPhpEnabled and
|
||||||
|
* allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
|
||||||
|
* This allows anonymous users to download legally doubtful internet content which on
|
||||||
|
* tracing back appears to being downloaded by your server, or allows malicious php code
|
||||||
|
* in remote html pages to be executed by your server with your account privileges.
|
||||||
|
*
|
||||||
|
* This setting may increase the risk of system exploit. Do not change
|
||||||
|
* this settings without understanding the consequences. Additional
|
||||||
|
* documentation is available on the dompdf wiki at:
|
||||||
|
* https://github.com/dompdf/dompdf/wiki
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_remote' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of allowed remote hosts
|
||||||
|
*
|
||||||
|
* Each value of the array must be a valid hostname.
|
||||||
|
*
|
||||||
|
* This will be used to filter which resources can be loaded in combination with
|
||||||
|
* isRemoteEnabled. If enable_remote is FALSE, then this will have no effect.
|
||||||
|
*
|
||||||
|
* Leave to NULL to allow any remote host.
|
||||||
|
*
|
||||||
|
* @var array|null
|
||||||
|
*/
|
||||||
|
'allowed_remote_hosts' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ratio applied to the fonts height to be more like browsers' line height
|
||||||
|
*/
|
||||||
|
'font_height_ratio' => 1.1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the HTML5 Lib parser
|
||||||
|
*
|
||||||
|
* @deprecated This feature is now always on in dompdf 2.x
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_html5_parser' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the connection options for every queue backend
|
||||||
|
| used by your application. An example configuration is provided for
|
||||||
|
| each backend supported by Laravel. You're also free to add more.
|
||||||
|
|
|
||||||
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||||
|
| "deferred", "background", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sync' => [
|
||||||
|
'driver' => 'sync',
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||||
|
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||||
|
'queue' => env('DB_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'beanstalkd' => [
|
||||||
|
'driver' => 'beanstalkd',
|
||||||
|
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||||
|
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => 0,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqs' => [
|
||||||
|
'driver' => 'sqs',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||||
|
'queue' => env('SQS_QUEUE', 'default'),
|
||||||
|
'suffix' => env('SQS_SUFFIX'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => null,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'deferred' => [
|
||||||
|
'driver' => 'deferred',
|
||||||
|
],
|
||||||
|
|
||||||
|
'background' => [
|
||||||
|
'driver' => 'background',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'connections' => [
|
||||||
|
'database',
|
||||||
|
'deferred',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Batching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following options configure the database and table that store job
|
||||||
|
| batching information. These options can be updated to any database
|
||||||
|
| connection and table which has been defined by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'batching' => [
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'job_batches',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Failed Queue Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure the behavior of failed queue job logging so you
|
||||||
|
| can control how and where failed jobs are stored. Laravel ships with
|
||||||
|
| support for storing failed jobs in a simple file or in a database.
|
||||||
|
|
|
||||||
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'failed' => [
|
||||||
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'failed_jobs',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'key' => env('RESEND_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'notifications' => [
|
||||||
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Lifetime
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the number of minutes that you wish the session
|
||||||
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
|
| to expire immediately when the browser is closed then you may
|
||||||
|
| indicate that via the expire_on_close configuration option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Encryption
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to easily specify that all of your session data
|
||||||
|
| should be encrypted before it's stored. All encryption is performed
|
||||||
|
| automatically by Laravel and you may use the session like normal.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session File Location
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the "file" session driver, the session files are placed
|
||||||
|
| on disk. The default storage location is defined here; however, you
|
||||||
|
| are free to provide another location where they should be stored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" or "redis" session drivers, you may specify a
|
||||||
|
| connection that should be used to manage these sessions. This should
|
||||||
|
| correspond to a connection in your database configuration options.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connection' => env('SESSION_CONNECTION'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" session driver, you may specify the table to
|
||||||
|
| be used to store sessions. Of course, a sensible default is defined
|
||||||
|
| for you; however, you're welcome to change this to another table.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using one of the framework's cache driven session backends, you may
|
||||||
|
| define the cache store which should be used to store the session data
|
||||||
|
| between requests. This must match one of your defined cache stores.
|
||||||
|
|
|
||||||
|
| Affects: "dynamodb", "memcached", "redis"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Sweeping Lottery
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some session drivers must manually sweep their storage location to get
|
||||||
|
| rid of old sessions from storage. Here are the chances that it will
|
||||||
|
| happen on a given request. By default, the odds are 2 out of 100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may change the name of the session cookie that is created by
|
||||||
|
| the framework. Typically, you should not need to change this value
|
||||||
|
| since doing so does not grant a meaningful security improvement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cookie' => env(
|
||||||
|
'SESSION_COOKIE',
|
||||||
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The session cookie path determines the path for which the cookie will
|
||||||
|
| be regarded as available. Typically, this will be the root path of
|
||||||
|
| your application, but you're free to change this when necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the domain and subdomains the session cookie is
|
||||||
|
| available to. By default, the cookie will be available to the root
|
||||||
|
| domain without subdomains. Typically, this shouldn't be changed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTPS Only Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By setting this option to true, session cookies will only be sent back
|
||||||
|
| to the server if the browser has a HTTPS connection. This will keep
|
||||||
|
| the cookie from being sent to you when it can't be done securely.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTP Access Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
|
| value of the cookie and the cookie will only be accessible through
|
||||||
|
| the HTTP protocol. It's unlikely you should disable this option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Same-Site Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines how your cookies behave when cross-site requests
|
||||||
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
|
| will set this value to "lax" to permit secure cross-site requests.
|
||||||
|
|
|
||||||
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||||
|
|
|
||||||
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Partitioned Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will tie the cookie to the top-level site for
|
||||||
|
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||||
|
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
*.sqlite*
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||||
|
*/
|
||||||
|
class UserFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current password being used by the factory.
|
||||||
|
*/
|
||||||
|
protected static ?string $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->name(),
|
||||||
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model's email address should be unverified.
|
||||||
|
*/
|
||||||
|
public function unverified(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'email_verified_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
// database/migrations/2026_02_24_xxxxxx_create_reviews_table.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('reviews', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->text('review')->nullable(); // Review Penggunaan Aplikasi S-Learn
|
||||||
|
$table->text('text_final')->nullable();
|
||||||
|
$table->integer('score')->default(0);
|
||||||
|
$table->string('sentiment', 20)->nullable(); // Sentimen (positif/negatif)
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Index untuk optimasi query
|
||||||
|
$table->index('sentiment');
|
||||||
|
$table->index('score');
|
||||||
|
$table->index('created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('reviews');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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('users', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('password');
|
||||||
|
$table->string('role')->default('user'); // admin, user
|
||||||
|
$table->string('avatar')->nullable();
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?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('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?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('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->integer('expiration')->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class AdminUserSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Membuat Akun Admin
|
||||||
|
User::create([
|
||||||
|
'name' => 'Administrator System',
|
||||||
|
'email' => 'admin@gmail.com',
|
||||||
|
'password' => Hash::make('password123'), // Gantilah dengan password yang aman
|
||||||
|
'role' => 'admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Membuat Akun User Biasa (untuk testing)
|
||||||
|
User::create([
|
||||||
|
'name' => 'Regular User',
|
||||||
|
'email' => 'user@gmail.com',
|
||||||
|
'password' => Hash::make('password123'),
|
||||||
|
'role' => 'user',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Review;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class DatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Buat user admin
|
||||||
|
User::create([
|
||||||
|
'name' => 'Admin S-Learn',
|
||||||
|
'email' => 'admin@slearn.com',
|
||||||
|
'password' => Hash::make('admin123'),
|
||||||
|
'role' => 'admin',
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Buat user biasa (50 user)
|
||||||
|
User::factory(50)->create();
|
||||||
|
|
||||||
|
// Panggil seeder review jika ada
|
||||||
|
$this->call([
|
||||||
|
AdminUserSeeder::class, // Jika Anda punya seeder khusus admin
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Catatan: Untuk review, sebaiknya buat seeder terpisah
|
||||||
|
// Review::factory(200)->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"alpinejs": "^3.4.2",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"tailwindcss": "^3.1.0",
|
||||||
|
"vite": "^7.0.7"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chartjs-plugin-datalabels": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory>tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>app</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
<env name="DB_CONNECTION" value="sqlite"/>
|
||||||
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
|
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews -Indexes
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Handle Authorization Header
|
||||||
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
# Handle X-XSRF-Token Header
|
||||||
|
RewriteCond %{HTTP:x-xsrf-token} .
|
||||||
|
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||||
|
|
||||||
|
# Redirect Trailing Slashes If Not A Folder...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [L,R=301]
|
||||||
|
|
||||||
|
# Send Requests To Front Controller...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
</IfModule>
|
||||||
|
|
@ -0,0 +1,411 @@
|
||||||
|
/* ===== RESET & BASE ===== */
|
||||||
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f8fafc !important;
|
||||||
|
font-family: 'Figtree', 'Segoe UI', sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== SIDEBAR STYLES ===== */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 280px;
|
||||||
|
background: #1e3a8a;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
z-index: 50;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 4px 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Header */
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo-image {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo-text {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Menu Items */
|
||||||
|
.sidebar-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.875rem 1.5rem;
|
||||||
|
margin: 0.5rem 1rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item i {
|
||||||
|
width: 24px;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item:hover i {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item.active {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item.active i {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 4px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logout button special style */
|
||||||
|
.sidebar-menu-item.logout {
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item.logout i {
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item.logout:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu-item.logout:hover i {
|
||||||
|
color: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Footer */
|
||||||
|
.sidebar-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user-role {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MAIN CONTENT ===== */
|
||||||
|
.main-content {
|
||||||
|
margin-left: 280px;
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar Sticky */
|
||||||
|
.navbar-sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 40;
|
||||||
|
background: #1e3a8a;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MOBILE MENU BUTTON ===== */
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 100;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: #1e3a8a;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(30, 58, 138, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE ===== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== INPUT STYLES (hanya jika tidak menggunakan Tailwind) ===== */
|
||||||
|
input, select, textarea {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid rgba(30, 58, 138, 0.2);
|
||||||
|
color: #1e293b;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus, textarea:focus {
|
||||||
|
border-color: #1e3a8a;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TABLE SCROLL (khusus untuk scroll) ===== */
|
||||||
|
.table-scroll {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: rgba(30, 58, 138, 0.05);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== CUSTOM SCROLLBAR ===== */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #1e3a8a;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MODAL (hanya untuk struktur) ===== */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ANIMATIONS ===== */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-custom {
|
||||||
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in {
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 3px solid rgba(30, 58, 138, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 3px solid #1e3a8a;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== PRINT STYLES ===== */
|
||||||
|
@media print {
|
||||||
|
.sidebar,
|
||||||
|
.navbar-sticky,
|
||||||
|
.mobile-menu-btn,
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== UTILITY CLASSES (hanya yang tidak ada di Tailwind) ===== */
|
||||||
|
.text-white-70 {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white-50 {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white-10 {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white-20 {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Determine if the application is in maintenance mode...
|
||||||
|
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||||
|
require $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the request...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
|
$app->handleRequest(Request::capture());
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// resources/js/app.js
|
||||||
|
import './bootstrap';
|
||||||
|
import Alpine from 'alpinejs';
|
||||||
|
import Chart from 'chart.js/auto';
|
||||||
|
import { ChartManager } from './charts';
|
||||||
|
|
||||||
|
window.Alpine = Alpine;
|
||||||
|
window.Chart = Chart;
|
||||||
|
window.ChartManager = ChartManager;
|
||||||
|
|
||||||
|
Alpine.start();
|
||||||
|
|
||||||
|
console.log('App.js loaded with ChartManager');
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
window.axios = axios;
|
||||||
|
|
||||||
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
// resources/js/charts.js
|
||||||
|
class ChartManager {
|
||||||
|
constructor() {
|
||||||
|
this.charts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
init(data) {
|
||||||
|
console.log('ChartManager initialized with data:', data);
|
||||||
|
this.destroyAll();
|
||||||
|
this.createBarChart(data);
|
||||||
|
this.createPieChart(data);
|
||||||
|
this.createScoreChart(data);
|
||||||
|
this.createMetricsChart(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyAll() {
|
||||||
|
Object.values(this.charts).forEach(chart => {
|
||||||
|
if (chart) chart.destroy();
|
||||||
|
});
|
||||||
|
this.charts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
createBarChart(data) {
|
||||||
|
const canvas = document.getElementById('barChart');
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const total = data.positif + data.negatif;
|
||||||
|
const positifPercent = total > 0 ? ((data.positif / total) * 100).toFixed(1) : 0;
|
||||||
|
const negatifPercent = total > 0 ? ((data.negatif / total) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
this.charts.barChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Positif', 'Negatif'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Sentimen',
|
||||||
|
data: [data.positif, data.negatif],
|
||||||
|
backgroundColor: ['#10b981', '#ef4444'],
|
||||||
|
borderColor: ['#059669', '#dc2626'],
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
barPercentage: 0.7
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => {
|
||||||
|
const value = context.raw;
|
||||||
|
const percentage = context.dataIndex === 0 ? positifPercent : negatifPercent;
|
||||||
|
return `Jumlah: ${value} (${percentage}%)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createPieChart(data) {
|
||||||
|
const canvas = document.getElementById('pieChart');
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const total = data.positif + data.negatif;
|
||||||
|
const positifPercent = total > 0 ? ((data.positif / total) * 100).toFixed(1) : 0;
|
||||||
|
const negatifPercent = total > 0 ? ((data.negatif / total) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
this.charts.pieChart = new Chart(ctx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: [`Positif (${positifPercent}%)`, `Negatif (${negatifPercent}%)`],
|
||||||
|
datasets: [{
|
||||||
|
data: [data.positif, data.negatif],
|
||||||
|
backgroundColor: ['#10b981', '#ef4444'],
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
borderWidth: 2,
|
||||||
|
hoverOffset: 8
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: { color: '#ffffff', font: { size: 12 }, padding: 20 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cutout: '60%'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createScoreChart(data) {
|
||||||
|
const canvas = document.getElementById('scoreChart');
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
this.charts.scoreChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['0-20', '21-40', '41-60', '61-80', '81-100'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Distribusi Score',
|
||||||
|
data: data.scoreDistribution,
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
borderWidth: 3,
|
||||||
|
pointBackgroundColor: '#3b82f6',
|
||||||
|
pointBorderColor: '#ffffff',
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointHoverRadius: 8,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => `Jumlah: ${context.raw} data`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createMetricsChart(data) {
|
||||||
|
const canvas = document.getElementById('groupedMetricsChart');
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
this.charts.metricsChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Precision', 'Recall', 'F1-Score'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Negatif',
|
||||||
|
data: data.metrics.negatif,
|
||||||
|
backgroundColor: '#ef4444',
|
||||||
|
borderRadius: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Positif',
|
||||||
|
data: data.metrics.positif,
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
borderRadius: 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: { color: '#ffffff' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => `${context.dataset.label}: ${(context.raw * 100).toFixed(1)}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 1,
|
||||||
|
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||||
|
ticks: {
|
||||||
|
color: '#ffffff',
|
||||||
|
callback: (value) => (value * 100) + '%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export untuk digunakan di view
|
||||||
|
window.ChartManager = ChartManager;
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
// resources/js/charts.js
|
||||||
|
export class ChartManager {
|
||||||
|
constructor() {
|
||||||
|
this.charts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
init(data) {
|
||||||
|
console.log('ChartManager initialized with data:', data);
|
||||||
|
this.destroyAll();
|
||||||
|
this.createBarChart(data);
|
||||||
|
this.createPieChart(data);
|
||||||
|
this.createScoreChart(data);
|
||||||
|
this.createMetricsChart(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyAll() {
|
||||||
|
Object.values(this.charts).forEach(chart => {
|
||||||
|
if (chart) chart.destroy();
|
||||||
|
});
|
||||||
|
this.charts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
createBarChart(data) {
|
||||||
|
const canvas = document.getElementById('barChart');
|
||||||
|
if (!canvas) {
|
||||||
|
console.warn('Canvas barChart tidak ditemukan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const total = data.positif + data.negatif;
|
||||||
|
const positifPercent = total > 0 ? ((data.positif / total) * 100).toFixed(1) : 0;
|
||||||
|
const negatifPercent = total > 0 ? ((data.negatif / total) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
this.charts.barChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Positif', 'Negatif'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Sentimen',
|
||||||
|
data: [data.positif, data.negatif],
|
||||||
|
backgroundColor: ['#10b981', '#ef4444'],
|
||||||
|
borderColor: ['#059669', '#dc2626'],
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
barPercentage: 0.7
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => {
|
||||||
|
const value = context.raw;
|
||||||
|
const percentage = context.dataIndex === 0 ? positifPercent : negatifPercent;
|
||||||
|
return `Jumlah: ${value} (${percentage}%)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Bar chart created');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPieChart(data) {
|
||||||
|
const canvas = document.getElementById('pieChart');
|
||||||
|
if (!canvas) {
|
||||||
|
console.warn('Canvas pieChart tidak ditemukan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const total = data.positif + data.negatif;
|
||||||
|
const positifPercent = total > 0 ? ((data.positif / total) * 100).toFixed(1) : 0;
|
||||||
|
const negatifPercent = total > 0 ? ((data.negatif / total) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
this.charts.pieChart = new Chart(ctx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: [`Positif (${positifPercent}%)`, `Negatif (${negatifPercent}%)`],
|
||||||
|
datasets: [{
|
||||||
|
data: [data.positif, data.negatif],
|
||||||
|
backgroundColor: ['#10b981', '#ef4444'],
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
borderWidth: 2,
|
||||||
|
hoverOffset: 8
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: { color: '#ffffff', font: { size: 12 }, padding: 20 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cutout: '60%'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Pie chart created');
|
||||||
|
}
|
||||||
|
|
||||||
|
createScoreChart(data) {
|
||||||
|
const canvas = document.getElementById('scoreChart');
|
||||||
|
if (!canvas) {
|
||||||
|
console.warn('Canvas scoreChart tidak ditemukan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
this.charts.scoreChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['0-20', '21-40', '41-60', '61-80', '81-100'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Distribusi Score',
|
||||||
|
data: data.scoreDistribution,
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
borderWidth: 3,
|
||||||
|
pointBackgroundColor: '#3b82f6',
|
||||||
|
pointBorderColor: '#ffffff',
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointHoverRadius: 8,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => `Jumlah: ${context.raw} data`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Score chart created');
|
||||||
|
}
|
||||||
|
|
||||||
|
createMetricsChart(data) {
|
||||||
|
const canvas = document.getElementById('groupedMetricsChart');
|
||||||
|
if (!canvas) {
|
||||||
|
console.warn('Canvas groupedMetricsChart tidak ditemukan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
this.charts.metricsChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Precision', 'Recall', 'F1-Score'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Negatif',
|
||||||
|
data: data.metrics.negatif,
|
||||||
|
backgroundColor: '#ef4444',
|
||||||
|
borderRadius: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Positif',
|
||||||
|
data: data.metrics.positif,
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
borderRadius: 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: { color: '#ffffff' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => `${context.dataset.label}: ${(context.raw * 100).toFixed(1)}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 1,
|
||||||
|
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||||
|
ticks: {
|
||||||
|
color: '#ffffff',
|
||||||
|
callback: (value) => (value * 100) + '%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { color: '#ffffff' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Metrics chart created');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<title>{{ config('app.name', 'S-Learn Sentiment Analysis') }}</title>
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700,800,900&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Font Awesome Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Animate.css -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
|
||||||
|
|
||||||
|
<!-- Scripts - Tailwind CSS via Vite -->
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/style.css') }}">
|
||||||
|
|
||||||
|
<!-- Alpine.js -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
|
<!-- HAPUS CHART.JS DARI SINI! -->
|
||||||
|
<!-- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> -->
|
||||||
|
<!-- <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0/dist/chartjs-plugin-datalabels.min.js"></script> -->
|
||||||
|
</head>
|
||||||
|
<body class="font-sans antialiased">
|
||||||
|
<!-- Mobile Menu Button -->
|
||||||
|
<button class="mobile-menu-btn" onclick="toggleSidebar()" id="mobileMenuBtn">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Sidebar - Biru Navy -->
|
||||||
|
<div class="sidebar" id="sidebar">
|
||||||
|
<!-- Sidebar Header - Tinggi disamakan dengan navbar -->
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<!-- Logo Gambar dari public/images/logo.png -->
|
||||||
|
<img src="{{ asset('images/logo.png') }}" alt="S-Learn Logo" class="sidebar-logo-image">
|
||||||
|
<div>
|
||||||
|
<div class="sidebar-logo-text">S-Learn Analytics</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<div class="px-3">
|
||||||
|
<!-- Dashboard Menu -->
|
||||||
|
<a href="{{ route('dashboard') }}" class="sidebar-menu-item {{ request()->routeIs('dashboard') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-chart-pie"></i>
|
||||||
|
<span>Dashboard Sentimen</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Data Management Menu -->
|
||||||
|
<a href="{{ route('data.management') }}" class="sidebar-menu-item {{ request()->routeIs('data.management') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
<span>Data Management</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Logout Menu -->
|
||||||
|
<form method="POST" action="{{ route('logout') }}" class="block">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="sidebar-menu-item logout w-full text-left">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
|
<span>Logout</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar Footer - User Info -->
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="sidebar-user-info">
|
||||||
|
<div class="sidebar-user-avatar">
|
||||||
|
{{ substr(auth()->user()->name ?? 'U', 0, 1) }}
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-user-details">
|
||||||
|
<div class="sidebar-user-name">{{ auth()->user()->name ?? 'User' }}</div>
|
||||||
|
<div class="sidebar-user-role">
|
||||||
|
<i class="fas fa-circle text-green-400 text-[6px] mr-1"></i>
|
||||||
|
{{ auth()->user()->role ?? 'Administrator' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-center mt-3 text-white/50">
|
||||||
|
© {{ date('Y') }} S-Learn Analytics
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Sticky Navbar - Biru Navy -->
|
||||||
|
<nav class="navbar-sticky px-6">
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<!-- Left side - Page Title -->
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<i class="fas fa-bars text-white/70 text-xl cursor-pointer hover:text-white transition md:hidden" onclick="toggleSidebar()"></i>
|
||||||
|
<div id="pageTitle">
|
||||||
|
@if(request()->routeIs('dashboard'))
|
||||||
|
<h1 class="text-xl font-bold text-white flex items-center">
|
||||||
|
<i class="fas fa-chart-line text-white/80 mr-2"></i>
|
||||||
|
Dashboard
|
||||||
|
</h1>
|
||||||
|
@elseif(request()->routeIs('data.management'))
|
||||||
|
<h1 class="text-xl font-bold text-white flex items-center">
|
||||||
|
<i class="fas fa-database text-white/80 mr-2"></i>
|
||||||
|
Data Management
|
||||||
|
</h1>
|
||||||
|
@else
|
||||||
|
<h1 class="text-xl font-bold text-white">S-Learn Analytics</h1>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side - Quick Actions -->
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<!-- Refresh Button -->
|
||||||
|
<button onclick="location.reload()" class="p-2 rounded-lg hover:bg-white/10 transition">
|
||||||
|
<i class="fas fa-sync-alt text-white/70 hover:text-white"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- User Dropdown -->
|
||||||
|
<div class="flex items-center space-x-2 ml-2 pl-2 border-l border-white/20">
|
||||||
|
<span class="text-sm font-medium text-white/90">{{ auth()->user()->name ?? 'User' }}</span>
|
||||||
|
<div class="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center text-white font-bold text-sm">
|
||||||
|
{{ substr(auth()->user()->name ?? 'U', 0, 1) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
<main>
|
||||||
|
{{ $slot }}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script>
|
||||||
|
// Toggle sidebar function
|
||||||
|
function toggleSidebar() {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const menuBtn = document.getElementById('mobileMenuBtn');
|
||||||
|
sidebar.classList.toggle('open');
|
||||||
|
|
||||||
|
if (sidebar.classList.contains('open')) {
|
||||||
|
menuBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||||
|
} else {
|
||||||
|
menuBtn.innerHTML = '<i class="fas fa-bars"></i>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close sidebar when clicking outside on mobile
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const menuBtn = document.getElementById('mobileMenuBtn');
|
||||||
|
const isClickInsideSidebar = sidebar.contains(event.target);
|
||||||
|
const isClickOnMenuBtn = menuBtn.contains(event.target);
|
||||||
|
|
||||||
|
if (window.innerWidth <= 768 && sidebar.classList.contains('open') && !isClickInsideSidebar && !isClickOnMenuBtn) {
|
||||||
|
sidebar.classList.remove('open');
|
||||||
|
menuBtn.innerHTML = '<i class="fas fa-bars"></i>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const menuBtn = document.getElementById('mobileMenuBtn');
|
||||||
|
|
||||||
|
if (window.innerWidth > 768) {
|
||||||
|
sidebar.classList.remove('open');
|
||||||
|
menuBtn.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
menuBtn.style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tambahkan class untuk styling
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Hapus class dark dari html
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
|
||||||
|
// Tambahkan class untuk styling
|
||||||
|
document.body.classList.add('bg-gray-50');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Dashboard Sentimen S-Learn</title>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-dark bg-primary">
|
||||||
|
<div class="container">
|
||||||
|
<span class="navbar-brand mb-0 h1">
|
||||||
|
Dashboard Analisis Sentimen S-Learn
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
@yield('content')
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<title>{{ config('app.name', 'S-Learn') }} - Login</title>
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="font-sans antialiased bg-gray-50">
|
||||||
|
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0">
|
||||||
|
<div class="w-full sm:max-w-md mt-6 px-6 py-8 bg-white shadow-xl rounded-2xl">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="text-center mt-8 text-sm text-gray-500">
|
||||||
|
© {{ date('Y') }} S-Learn. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
|
||||||
|
<!-- Primary Navigation Menu -->
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-between h-16">
|
||||||
|
<div class="flex">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="shrink-0 flex items-center">
|
||||||
|
<a href="{{ route('dashboard') }}">
|
||||||
|
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Links -->
|
||||||
|
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||||
|
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
|
{{ __('Dashboard') }}
|
||||||
|
</x-nav-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Dropdown -->
|
||||||
|
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||||
|
<x-dropdown align="right" width="48">
|
||||||
|
<x-slot name="trigger">
|
||||||
|
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
|
||||||
|
<div>{{ Auth::user()->name }}</div>
|
||||||
|
|
||||||
|
<div class="ms-1">
|
||||||
|
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<x-slot name="content">
|
||||||
|
<x-dropdown-link :href="route('profile.edit')">
|
||||||
|
{{ __('Profile') }}
|
||||||
|
</x-dropdown-link>
|
||||||
|
|
||||||
|
<!-- Authentication -->
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<x-dropdown-link :href="route('logout')"
|
||||||
|
onclick="event.preventDefault();
|
||||||
|
this.closest('form').submit();">
|
||||||
|
{{ __('Log Out') }}
|
||||||
|
</x-dropdown-link>
|
||||||
|
</form>
|
||||||
|
</x-slot>
|
||||||
|
</x-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hamburger -->
|
||||||
|
<div class="-me-2 flex items-center sm:hidden">
|
||||||
|
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
|
||||||
|
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Responsive Navigation Menu -->
|
||||||
|
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||||
|
<div class="pt-2 pb-3 space-y-1">
|
||||||
|
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
|
{{ __('Dashboard') }}
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Responsive Settings Options -->
|
||||||
|
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||||
|
<div class="px-4">
|
||||||
|
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
|
||||||
|
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 space-y-1">
|
||||||
|
<x-responsive-nav-link :href="route('profile.edit')">
|
||||||
|
{{ __('Profile') }}
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
|
||||||
|
<!-- Authentication -->
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<x-responsive-nav-link :href="route('logout')"
|
||||||
|
onclick="event.preventDefault();
|
||||||
|
this.closest('form').submit();">
|
||||||
|
{{ __('Log Out') }}
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<div class="mb-4 text-sm text-gray-600">
|
||||||
|
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('password.confirm') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required autocomplete="current-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Confirm') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="bg-[#1e3a8a] w-20 h-20 rounded-2xl mx-auto mb-4 flex items-center justify-center shadow-lg">
|
||||||
|
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900">Lupa Password?</h2>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">Tenang, kami akan bantu reset password Anda</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6 text-sm text-gray-600 bg-blue-50 p-4 rounded-xl">
|
||||||
|
<p>Masukkan email Anda dan kami akan mengirimkan link reset password.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Status -->
|
||||||
|
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('password.email') }}" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus
|
||||||
|
class="pl-10 w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#1e3a8a] focus:border-[#1e3a8a] transition duration-200 @error('email') border-red-500 @enderror"
|
||||||
|
placeholder="admin@gmail.com">
|
||||||
|
</div>
|
||||||
|
@error('email')
|
||||||
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<button type="submit" class="w-full bg-[#1e3a8a] text-white py-3 px-4 rounded-xl hover:bg-[#2b4a9e] focus:ring-4 focus:ring-[#1e3a8a]/50 transition duration-200 font-medium text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||||
|
<span class="flex items-center justify-center">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
Kirim Link Reset Password
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Back to Login -->
|
||||||
|
<div class="text-center mt-6">
|
||||||
|
<a href="{{ route('login') }}" class="text-sm text-[#1e3a8a] hover:text-[#2b4a9e] hover:underline transition duration-200 flex items-center justify-center">
|
||||||
|
<svg class="w-4 h-4 mr-1" 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"></path>
|
||||||
|
</svg>
|
||||||
|
Kembali ke halaman login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<!-- Session Status -->
|
||||||
|
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||||
|
|
||||||
|
<!-- Header dengan Logo -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="bg-[#1e3a8a] w-20 h-20 rounded-2xl mx-auto mb-4 flex items-center justify-center shadow-lg">
|
||||||
|
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900">Selamat Datang Kembali</h2>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">Silakan login ke akun S-Learn Anda</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('login') }}" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus
|
||||||
|
autocomplete="username"
|
||||||
|
class="pl-10 w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#1e3a8a] focus:border-[#1e3a8a] transition duration-200 @error('email') border-red-500 @enderror"
|
||||||
|
placeholder="admin@gmail.com">
|
||||||
|
</div>
|
||||||
|
@error('email')
|
||||||
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-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 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input id="password" type="password" name="password" required autocomplete="current-password"
|
||||||
|
class="pl-10 w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#1e3a8a] focus:border-[#1e3a8a] transition duration-200 @error('password') border-red-500 @enderror"
|
||||||
|
placeholder="********">
|
||||||
|
</div>
|
||||||
|
@error('password')
|
||||||
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remember Me & Forgot Password -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="remember"
|
||||||
|
class="rounded border-gray-300 text-[#1e3a8a] shadow-sm focus:ring-[#1e3a8a]">
|
||||||
|
<span class="ml-2 text-sm text-gray-600">Ingat saya</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if (Route::has('password.request'))
|
||||||
|
<a href="{{ route('password.request') }}"
|
||||||
|
class="text-sm text-[#1e3a8a] hover:text-[#2b4a9e] hover:underline transition duration-200">
|
||||||
|
Lupa password?
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Button -->
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-[#1e3a8a] text-white py-3 px-4 rounded-xl hover:bg-[#2b4a9e] focus:ring-4 focus:ring-[#1e3a8a]/50 transition duration-200 font-medium text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||||
|
<span class="flex items-center justify-center">
|
||||||
|
<svg class="w-5 h-5 mr-2" 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>
|
||||||
|
Masuk
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Link ke Register (Jika diperlukan) -->
|
||||||
|
@if (Route::has('register'))
|
||||||
|
<div class="text-center mt-6 pt-6 border-t border-gray-200">
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
Belum punya akun?
|
||||||
|
<a href="{{ route('register') }}"
|
||||||
|
class="font-medium text-[#1e3a8a] hover:text-[#2b4a9e] hover:underline transition duration-200">
|
||||||
|
Daftar sekarang
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="bg-[#1e3a8a] w-20 h-20 rounded-2xl mx-auto mb-4 flex items-center justify-center shadow-lg">
|
||||||
|
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900">Buat Akun Baru</h2>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">Daftar untuk menggunakan S-Learn</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('register') }}" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">Nama Lengkap</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input id="name" type="text" name="name" value="{{ old('name') }}" required autofocus autocomplete="name"
|
||||||
|
class="pl-10 w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#1e3a8a] focus:border-[#1e3a8a] transition duration-200 @error('name') border-red-500 @enderror"
|
||||||
|
placeholder="John Doe">
|
||||||
|
</div>
|
||||||
|
@error('name')
|
||||||
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input id="email" type="email" name="email" value="{{ old('email') }}" required autocomplete="username"
|
||||||
|
class="pl-10 w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#1e3a8a] focus:border-[#1e3a8a] transition duration-200 @error('email') border-red-500 @enderror"
|
||||||
|
placeholder="user@example.com">
|
||||||
|
</div>
|
||||||
|
@error('email')
|
||||||
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-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 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input id="password" type="password" name="password" required autocomplete="new-password"
|
||||||
|
class="pl-10 w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#1e3a8a] focus:border-[#1e3a8a] transition duration-200 @error('password') border-red-500 @enderror"
|
||||||
|
placeholder="Minimal 8 karakter">
|
||||||
|
</div>
|
||||||
|
@error('password')
|
||||||
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password -->
|
||||||
|
<div>
|
||||||
|
<label for="password_confirmation" class="block text-sm font-medium text-gray-700 mb-2">Konfirmasi Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input id="password_confirmation" type="password" name="password_confirmation" required autocomplete="new-password"
|
||||||
|
class="pl-10 w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#1e3a8a] focus:border-[#1e3a8a] transition duration-200"
|
||||||
|
placeholder="Ketik ulang password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Register Button -->
|
||||||
|
<button type="submit" class="w-full bg-[#1e3a8a] text-white py-3 px-4 rounded-xl hover:bg-[#2b4a9e] focus:ring-4 focus:ring-[#1e3a8a]/50 transition duration-200 font-medium text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||||
|
<span class="flex items-center justify-center">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path>
|
||||||
|
</svg>
|
||||||
|
Daftar
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Link to Login -->
|
||||||
|
<div class="text-center mt-6 pt-6 border-t border-gray-200">
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
Sudah punya akun?
|
||||||
|
<a href="{{ route('login') }}" class="font-medium text-[#1e3a8a] hover:text-[#2b4a9e] hover:underline transition duration-200">
|
||||||
|
Login disini
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<form method="POST" action="{{ route('password.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Password Reset Token -->
|
||||||
|
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
|
||||||
|
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password_confirmation" required autocomplete="new-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-4">
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Reset Password') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<div class="mb-4 text-sm text-gray-600">
|
||||||
|
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('status') == 'verification-link-sent')
|
||||||
|
<div class="mb-4 font-medium text-sm text-green-600">
|
||||||
|
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<form method="POST" action="{{ route('verification.send') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Resend Verification Email') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<button type="submit" 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">
|
||||||
|
{{ __('Log Out') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||||
|
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1,7 @@
|
||||||
|
@props(['status'])
|
||||||
|
|
||||||
|
@if ($status)
|
||||||
|
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
|
||||||
|
{{ $status }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$alignmentClasses = match ($align) {
|
||||||
|
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
|
||||||
|
'top' => 'origin-top',
|
||||||
|
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
|
||||||
|
};
|
||||||
|
|
||||||
|
$width = match ($width) {
|
||||||
|
'48' => 'w-48',
|
||||||
|
default => $width,
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||||
|
<div @click="open = ! open">
|
||||||
|
{{ $trigger }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="open"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
|
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
|
||||||
|
style="display: none;"
|
||||||
|
@click="open = false">
|
||||||
|
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
|
||||||
|
{{ $content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
@props(['messages'])
|
||||||
|
|
||||||
|
@if ($messages)
|
||||||
|
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
|
||||||
|
@foreach ((array) $messages as $message)
|
||||||
|
<li>{{ $message }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
@props(['value'])
|
||||||
|
|
||||||
|
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
|
||||||
|
{{ $value ?? $slot }}
|
||||||
|
</label>
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
@props([
|
||||||
|
'name',
|
||||||
|
'show' => false,
|
||||||
|
'maxWidth' => '2xl'
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$maxWidth = [
|
||||||
|
'sm' => 'sm:max-w-sm',
|
||||||
|
'md' => 'sm:max-w-md',
|
||||||
|
'lg' => 'sm:max-w-lg',
|
||||||
|
'xl' => 'sm:max-w-xl',
|
||||||
|
'2xl' => 'sm:max-w-2xl',
|
||||||
|
][$maxWidth];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-data="{
|
||||||
|
show: @js($show),
|
||||||
|
focusables() {
|
||||||
|
// All focusable element types...
|
||||||
|
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
||||||
|
return [...$el.querySelectorAll(selector)]
|
||||||
|
// All non-disabled elements...
|
||||||
|
.filter(el => ! el.hasAttribute('disabled'))
|
||||||
|
},
|
||||||
|
firstFocusable() { return this.focusables()[0] },
|
||||||
|
lastFocusable() { return this.focusables().slice(-1)[0] },
|
||||||
|
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
|
||||||
|
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
||||||
|
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
||||||
|
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
||||||
|
}"
|
||||||
|
x-init="$watch('show', value => {
|
||||||
|
if (value) {
|
||||||
|
document.body.classList.add('overflow-y-hidden');
|
||||||
|
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('overflow-y-hidden');
|
||||||
|
}
|
||||||
|
})"
|
||||||
|
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
|
||||||
|
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
|
||||||
|
x-on:close.stop="show = false"
|
||||||
|
x-on:keydown.escape.window="show = false"
|
||||||
|
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||||
|
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||||
|
x-show="show"
|
||||||
|
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
|
||||||
|
style="display: {{ $show ? 'block' : 'none' }};"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
x-show="show"
|
||||||
|
class="fixed inset-0 transform transition-all"
|
||||||
|
x-on:click="show = false"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="show"
|
||||||
|
class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
@props(['active'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$classes = ($active ?? false)
|
||||||
|
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||||
|
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</a>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
@props(['active'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$classes = ($active ?? false)
|
||||||
|
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||||
|
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</a>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@props(['disabled' => false])
|
||||||
|
|
||||||
|
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}>
|
||||||
|
|
@ -0,0 +1,620 @@
|
||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<div class="bg-[#1e3a8a] rounded-2xl shadow-lg p-6">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="bg-white/10 p-3 rounded-xl">
|
||||||
|
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-white">Dashboard Analisis Sentimen S-Learn</h1>
|
||||||
|
<p class="text-white/70 text-sm mt-1">Memantau kepuasan dan masukan pengguna platform S-Learn</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 md:mt-0 flex items-center space-x-3">
|
||||||
|
<div class="bg-white/10 px-4 py-2 rounded-lg">
|
||||||
|
<span class="text-white text-sm font-medium">Terakhir update: {{ now()->format('d M Y H:i') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$positif = $positif ?? 0;
|
||||||
|
$negatif = $negatif ?? 0;
|
||||||
|
$totalData = $totalData ?? ($positif + $negatif);
|
||||||
|
|
||||||
|
$score_0_20 = $score_0_20 ?? 0;
|
||||||
|
$score_21_40 = $score_21_40 ?? 0;
|
||||||
|
$score_41_60 = $score_41_60 ?? 0;
|
||||||
|
$score_61_80 = $score_61_80 ?? 0;
|
||||||
|
$score_81_100 = $score_81_100 ?? 0;
|
||||||
|
|
||||||
|
$formattedMetrics = $formattedMetrics ?? [];
|
||||||
|
$metricsNegatif = $formattedMetrics['negatif'] ?? ['precision'=>0,'recall'=>0,'f1'=>0];
|
||||||
|
$metricsPositif = $formattedMetrics['positif'] ?? ['precision'=>0,'recall'=>0,'f1'=>0];
|
||||||
|
|
||||||
|
$maintenance = $maintenance ?? [];
|
||||||
|
$maintenanceDetails = $maintenanceDetails ?? [];
|
||||||
|
|
||||||
|
// Hitung persentase untuk memudahkan pemahaman
|
||||||
|
$persentasePositif = $totalData > 0 ? round(($positif/$totalData)*100, 1) : 0;
|
||||||
|
$persentaseNegatif = $totalData > 0 ? round(($negatif/$totalData)*100, 1) : 0;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="py-8 bg-gray-50 min-h-screen"
|
||||||
|
x-data="{
|
||||||
|
openModal: false,
|
||||||
|
selectedCategory: '',
|
||||||
|
selectedData: [],
|
||||||
|
activeTab: 'ringkasan'
|
||||||
|
}">
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||||
|
|
||||||
|
<!-- ========== TAB NAVIGASI ========== -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-1 flex flex-wrap">
|
||||||
|
<button @click="activeTab = 'ringkasan'"
|
||||||
|
class="flex-1 py-3 px-4 rounded-lg text-sm font-medium transition-all"
|
||||||
|
:class="activeTab === 'ringkasan' ? 'bg-white text-[#1e3a8a] shadow-md' : 'text-white/80 hover:bg-white/10 hover:text-white'">
|
||||||
|
📊 Ringkasan
|
||||||
|
</button>
|
||||||
|
<button @click="activeTab = 'sentimen'"
|
||||||
|
class="flex-1 py-3 px-4 rounded-lg text-sm font-medium transition-all"
|
||||||
|
:class="activeTab === 'sentimen' ? 'bg-white text-[#1e3a8a] shadow-md' : 'text-white/80 hover:bg-white/10 hover:text-white'">
|
||||||
|
😊 Analisis Sentimen
|
||||||
|
</button>
|
||||||
|
<button @click="activeTab = 'akurasi'"
|
||||||
|
class="flex-1 py-3 px-4 rounded-lg text-sm font-medium transition-all"
|
||||||
|
:class="activeTab === 'akurasi' ? 'bg-white text-[#1e3a8a] shadow-md' : 'text-white/80 hover:bg-white/10 hover:text-white'">
|
||||||
|
📈 Akurasi & Prediksi
|
||||||
|
</button>
|
||||||
|
<button @click="activeTab = 'masalah'"
|
||||||
|
class="flex-1 py-3 px-4 rounded-lg text-sm font-medium transition-all"
|
||||||
|
:class="activeTab === 'masalah' ? 'bg-white text-[#1e3a8a] shadow-md' : 'text-white/80 hover:bg-white/10 hover:text-white'">
|
||||||
|
⚠️ Masalah & Saran
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== TAB RINGKASAN ========== -->
|
||||||
|
<div x-show="activeTab === 'ringkasan'" x-cloak>
|
||||||
|
<!-- Statistik Utama -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-5">
|
||||||
|
<p class="text-white/70 text-sm mb-1">Total Ulasan</p>
|
||||||
|
<p class="text-3xl font-bold text-white">{{ number_format($totalData) }}</p>
|
||||||
|
<p class="text-white/50 text-xs mt-1">Semua ulasan yang masuk</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-5">
|
||||||
|
<p class="text-white/70 text-sm mb-1">Ulasan Positif</p>
|
||||||
|
<p class="text-3xl font-bold text-green-300">{{ number_format($positif) }}</p>
|
||||||
|
<p class="text-white/50 text-xs mt-1">{{ $persentasePositif }}% dari total</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-5">
|
||||||
|
<p class="text-white/70 text-sm mb-1">Ulasan Negatif</p>
|
||||||
|
<p class="text-3xl font-bold text-red-300">{{ number_format($negatif) }}</p>
|
||||||
|
<p class="text-white/50 text-xs mt-1">{{ $persentaseNegatif }}% dari total</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-5">
|
||||||
|
<p class="text-white/70 text-sm mb-1">Tingkat Kepuasan</p>
|
||||||
|
<p class="text-3xl font-bold text-yellow-300">{{ $persentasePositif }}%</p>
|
||||||
|
<p class="text-white/50 text-xs mt-1">Pengguna puas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grafik Ringkasan -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<!-- Grafik Batang -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="font-semibold text-white">Perbandingan Ulasan</h3>
|
||||||
|
<span class="text-xs bg-white/20 text-white px-2 py-1 rounded-full">Grafik Batang</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-white/70 text-sm mb-4">Membandingkan jumlah ulasan positif dan negatif</p>
|
||||||
|
<div style="height:250px">
|
||||||
|
<canvas id="barChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-4 space-x-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="w-3 h-3 bg-green-400 rounded-full mr-2"></span>
|
||||||
|
<span class="text-sm text-white">Positif: {{ number_format($positif) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="w-3 h-3 bg-red-400 rounded-full mr-2"></span>
|
||||||
|
<span class="text-sm text-white">Negatif: {{ number_format($negatif) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grafik Lingkaran -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="font-semibold text-white">Proporsi Kepuasan</h3>
|
||||||
|
<span class="text-xs bg-white/20 text-white px-2 py-1 rounded-full">Grafik Lingkaran</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-white/70 text-sm mb-4">Persentase perbandingan ulasan</p>
|
||||||
|
<div style="height:250px">
|
||||||
|
<canvas id="pieChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="text-sm">
|
||||||
|
<span class="inline-block px-3 py-1 bg-green-500/20 text-green-300 rounded-full font-medium">{{ $persentasePositif }}% Puas</span>
|
||||||
|
<span class="mx-2 text-white/50">|</span>
|
||||||
|
<span class="inline-block px-3 py-1 bg-red-500/20 text-red-300 rounded-full font-medium">{{ $persentaseNegatif }}% Tidak Puas</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== TAB ANALISIS SENTIMEN ========== -->
|
||||||
|
<div x-show="activeTab === 'sentimen'" x-cloak>
|
||||||
|
<!-- Distribusi Tingkat Kepuasan -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6 mb-6">
|
||||||
|
<h3 class="font-semibold text-white mb-2">Tingkat Kepuasan Pengguna</h3>
|
||||||
|
<p class="text-white/70 text-sm mb-4">Skor 0-100 (Semakin tinggi angkanya, semakin puas pengguna)</p>
|
||||||
|
|
||||||
|
<!-- Kategori Skor -->
|
||||||
|
<div class="grid grid-cols-5 gap-2 mb-6">
|
||||||
|
<div class="text-center p-3 bg-red-500/20 rounded-lg">
|
||||||
|
<span class="text-xs text-red-300 font-medium block">0-20</span>
|
||||||
|
<span class="text-lg font-bold text-white">{{ $score_0_20 }}</span>
|
||||||
|
<span class="text-xs text-white/60 block">Sangat Tidak Puas</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-3 bg-orange-500/20 rounded-lg">
|
||||||
|
<span class="text-xs text-orange-300 font-medium block">21-40</span>
|
||||||
|
<span class="text-lg font-bold text-white">{{ $score_21_40 }}</span>
|
||||||
|
<span class="text-xs text-white/60 block">Tidak Puas</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-3 bg-yellow-500/20 rounded-lg">
|
||||||
|
<span class="text-xs text-yellow-300 font-medium block">41-60</span>
|
||||||
|
<span class="text-lg font-bold text-white">{{ $score_41_60 }}</span>
|
||||||
|
<span class="text-xs text-white/60 block">Netral</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-3 bg-lime-500/20 rounded-lg">
|
||||||
|
<span class="text-xs text-lime-300 font-medium block">61-80</span>
|
||||||
|
<span class="text-lg font-bold text-white">{{ $score_61_80 }}</span>
|
||||||
|
<span class="text-xs text-white/60 block">Puas</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-3 bg-green-500/20 rounded-lg">
|
||||||
|
<span class="text-xs text-green-300 font-medium block">81-100</span>
|
||||||
|
<span class="text-lg font-bold text-white">{{ $score_81_100 }}</span>
|
||||||
|
<span class="text-xs text-white/60 block">Sangat Puas</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grafik Garis -->
|
||||||
|
<div style="height:250px">
|
||||||
|
<canvas id="scoreChart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Box -->
|
||||||
|
<div class="mt-4 bg-white/10 p-4 rounded-lg">
|
||||||
|
<p class="text-sm text-white">
|
||||||
|
<span class="font-semibold">📊 Analisis:</span>
|
||||||
|
Grafik menunjukkan sebaran tingkat kepuasan.
|
||||||
|
@if($score_81_100 > $score_0_20)
|
||||||
|
Mayoritas pengguna merasa puas dengan layanan S-Learn (skor 81-100).
|
||||||
|
@else
|
||||||
|
Perlu perhatian pada area dengan skor rendah (0-40) untuk meningkatkan kepuasan.
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== TAB AKURASI & PREDIKSI ========== -->
|
||||||
|
<div x-show="activeTab === 'akurasi'" x-cloak>
|
||||||
|
<!-- Metrik Akurasi -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6 mb-6">
|
||||||
|
<h3 class="font-semibold text-white mb-2">Tingkat Akurasi Sistem</h3>
|
||||||
|
<p class="text-white/70 text-sm mb-4">Seberapa akurat sistem dalam memprediksi sentimen</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div class="bg-white/10 p-5 rounded-xl text-center">
|
||||||
|
<p class="text-sm text-blue-300 mb-1">Precision (Ketepatan)</p>
|
||||||
|
<p class="text-3xl font-bold text-white">{{ number_format($metricsPositif['precision'] * 100, 1) }}%</p>
|
||||||
|
<p class="text-xs text-white/50 mt-2">Seberapa tepat sistem memprediksi sentimen positif</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/10 p-5 rounded-xl text-center">
|
||||||
|
<p class="text-sm text-green-300 mb-1">Recall (Kelengkapan)</p>
|
||||||
|
<p class="text-3xl font-bold text-white">{{ number_format($metricsPositif['recall'] * 100, 1) }}%</p>
|
||||||
|
<p class="text-xs text-white/50 mt-2">Seberapa lengkap sistem menangkap sentimen positif</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/10 p-5 rounded-xl text-center">
|
||||||
|
<p class="text-sm text-purple-300 mb-1">F1-Score (Keseimbangan)</p>
|
||||||
|
<p class="text-3xl font-bold text-white">{{ number_format($metricsPositif['f1'] * 100, 1) }}%</p>
|
||||||
|
<p class="text-xs text-white/50 mt-2">Keseimbangan antara ketepatan dan kelengkapan</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grafik Metrik -->
|
||||||
|
<div style="height:250px" class="mb-6">
|
||||||
|
<canvas id="metricsChart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confusion Matrix (DIPERTAHANKAN) -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<h4 class="font-semibold text-white mb-3">📊 Matriks Prediksi (Confusion Matrix)</h4>
|
||||||
|
<p class="text-white/70 text-sm mb-4">Tabel ini menunjukkan detail hasil prediksi sistem</p>
|
||||||
|
|
||||||
|
@if(isset($cm) && count($cm) > 1)
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full max-w-2xl mx-auto border-collapse">
|
||||||
|
<!-- Header -->
|
||||||
|
<tr>
|
||||||
|
<td class="p-2"></td>
|
||||||
|
<td class="p-3 text-center font-semibold bg-white/20 text-white rounded-tl-lg" colspan="2">HASIL PREDIKSI SISTEM</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="p-2"></td>
|
||||||
|
<td class="p-3 text-center bg-red-500/20 font-medium text-red-300 rounded-bl-lg">Diprediksi NEGATIF</td>
|
||||||
|
<td class="p-3 text-center bg-green-500/20 font-medium text-green-300 rounded-br-lg">Diprediksi POSITIF</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Data -->
|
||||||
|
@foreach($cm as $i=>$row)
|
||||||
|
@if($i>0 && isset($row[1]) && isset($row[2]))
|
||||||
|
<tr>
|
||||||
|
<td class="p-3 font-medium bg-white/10 text-white rounded-l-lg">
|
||||||
|
{{ $i == 1 ? 'AKTUAL NEGATIF' : 'AKTUAL POSITIF' }}
|
||||||
|
<span class="block text-xs text-white/50">(Kenyataan sebenarnya)</span>
|
||||||
|
</td>
|
||||||
|
<td class="p-4 text-center bg-red-500/10 border-2 {{ $i==1 ? 'border-red-500/30' : 'border-orange-500/30' }}">
|
||||||
|
<span class="block text-2xl font-bold {{ $i==1 ? 'text-red-300' : 'text-orange-300' }}">{{ number_format($row[1]) }}</span>
|
||||||
|
<span class="text-xs text-white/70">
|
||||||
|
@if($i==1)
|
||||||
|
✓ Benar Negatif
|
||||||
|
<span class="block text-xs text-white/50">(Sistem benar memprediksi NEGATIF)</span>
|
||||||
|
@else
|
||||||
|
✗ Salah Negatif
|
||||||
|
<span class="block text-xs text-white/50">(Sistem salah memprediksi NEGATIF, padahal POSITIF)</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="p-4 text-center bg-green-500/10 border-2 {{ $i==2 ? 'border-green-500/30' : 'border-yellow-500/30' }}">
|
||||||
|
<span class="block text-2xl font-bold {{ $i==2 ? 'text-green-300' : 'text-yellow-300' }}">{{ number_format($row[2]) }}</span>
|
||||||
|
<span class="text-xs text-white/70">
|
||||||
|
@if($i==2)
|
||||||
|
✓ Benar Positif
|
||||||
|
<span class="block text-xs text-white/50">(Sistem benar memprediksi POSITIF)</span>
|
||||||
|
@else
|
||||||
|
✗ Salah Positif
|
||||||
|
<span class="block text-xs text-white/50">(Sistem salah memprediksi POSITIF, padahal NEGATIF)</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Penjelasan Sederhana -->
|
||||||
|
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="bg-green-500/10 p-4 rounded-lg">
|
||||||
|
<p class="font-semibold text-green-300 mb-2">✓ Prediksi Benar</p>
|
||||||
|
<ul class="text-sm text-white/80 space-y-2">
|
||||||
|
<li>• <span class="font-medium">Benar Positif ({{ number_format($cm[2][2] ?? 0) }})</span>: Sistem benar memprediksi POSITIF</li>
|
||||||
|
<li>• <span class="font-medium">Benar Negatif ({{ number_format($cm[1][1] ?? 0) }})</span>: Sistem benar memprediksi NEGATIF</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bg-red-500/10 p-4 rounded-lg">
|
||||||
|
<p class="font-semibold text-red-300 mb-2">✗ Prediksi Salah</p>
|
||||||
|
<ul class="text-sm text-white/80 space-y-2">
|
||||||
|
<li>• <span class="font-medium">Salah Positif ({{ number_format($cm[1][2] ?? 0) }})</span>: Sistem salah prediksi POSITIF</li>
|
||||||
|
<li>• <span class="font-medium">Salah Negatif ({{ number_format($cm[2][1] ?? 0) }})</span>: Sistem salah prediksi NEGATIF</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ringkasan -->
|
||||||
|
<div class="mt-4 bg-white/10 p-4 rounded-lg">
|
||||||
|
<p class="text-sm text-white">
|
||||||
|
<span class="font-semibold">📈 Ringkasan:</span><br>
|
||||||
|
Dari total {{ number_format($totalData) }} ulasan, sistem berhasil memprediksi dengan benar
|
||||||
|
<span class="font-bold text-green-300">{{ number_format(($cm[1][1] ?? 0) + ($cm[2][2] ?? 0)) }} data</span>
|
||||||
|
({{ $totalData > 0 ? round((($cm[1][1] ?? 0) + ($cm[2][2] ?? 0)) / $totalData * 100, 1) : 0 }}%)
|
||||||
|
dan salah memprediksi
|
||||||
|
<span class="font-bold text-red-300">{{ number_format(($cm[1][2] ?? 0) + ($cm[2][1] ?? 0)) }} data</span>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@else
|
||||||
|
<div class="text-center py-8 bg-white/10 rounded-lg">
|
||||||
|
<p class="text-white/70">Data confusion matrix belum tersedia</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== TAB MASALAH & SARAN ========== -->
|
||||||
|
<div x-show="activeTab === 'masalah'" x-cloak>
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6">
|
||||||
|
<h3 class="font-semibold text-white mb-2">Kategori Masalah & Saran Perbaikan</h3>
|
||||||
|
<p class="text-white/70 text-sm mb-6">Keluhan pengguna yang perlu mendapatkan perhatian</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
@forelse($maintenance as $kategori => $jumlah)
|
||||||
|
<div class="bg-white/10 rounded-lg p-5 hover:bg-white/20 transition-colors">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<h4 class="font-semibold text-white">{{ $kategori }}</h4>
|
||||||
|
<span class="bg-red-500/30 text-red-300 px-2 py-1 rounded text-xs font-bold">{{ $jumlah }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-white/70 mb-4">
|
||||||
|
{{ $jumlah }} pengguna melaporkan masalah ini
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if(!empty($maintenanceDetails[$kategori]))
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
openModal = true;
|
||||||
|
selectedCategory = '{{ $kategori }}';
|
||||||
|
selectedData = {{ json_encode($maintenanceDetails[$kategori]) }};
|
||||||
|
"
|
||||||
|
class="w-full px-4 py-2 bg-white/20 text-white rounded-lg text-sm hover:bg-white/30 transition-colors flex items-center justify-center">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||||
|
</svg>
|
||||||
|
Lihat Detail Keluhan
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="col-span-full text-center py-12">
|
||||||
|
<div class="bg-white/10 rounded-lg p-8">
|
||||||
|
<svg class="w-16 h-16 mx-auto text-green-300 mb-4" 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>
|
||||||
|
<p class="text-white font-medium">Tidak ada masalah yang dilaporkan</p>
|
||||||
|
<p class="text-sm text-white/50 mt-1">Semua sistem berjalan dengan baik</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== MODAL DETAIL KELUHAN ========== -->
|
||||||
|
<div x-show="openModal"
|
||||||
|
x-cloak
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed inset-0 z-50 overflow-y-auto"
|
||||||
|
style="display: none;">
|
||||||
|
|
||||||
|
<div class="fixed inset-0 bg-black/70" @click="openModal = false"></div>
|
||||||
|
|
||||||
|
<div class="relative min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div class="relative bg-[#1e3a8a] rounded-xl shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden">
|
||||||
|
|
||||||
|
<!-- Header Modal -->
|
||||||
|
<div class="bg-white/10 p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="bg-white/20 p-2 rounded-lg">
|
||||||
|
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-white">Detail Keluhan</h3>
|
||||||
|
<p class="text-white/70 text-sm" x-text="selectedCategory"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal = false" class="text-white/80 hover:text-white">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Konten Modal -->
|
||||||
|
<div class="p-6 overflow-y-auto max-h-[calc(80vh-120px)]">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<span class="text-sm text-white/70">Total Keluhan:</span>
|
||||||
|
<span class="bg-white/20 text-white px-3 py-1 rounded-full text-sm font-medium"
|
||||||
|
x-text="selectedData.length + ' ulasan'"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<template x-for="(item, index) in selectedData" :key="index">
|
||||||
|
<div class="bg-white/10 p-4 rounded-lg">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<span class="flex-shrink-0 w-6 h-6 bg-white/20 text-white rounded-full flex items-center justify-center text-xs font-bold"
|
||||||
|
x-text="index + 1"></span>
|
||||||
|
<p class="text-white/90 text-sm" x-text="item"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Modal -->
|
||||||
|
<div class="bg-white/10 px-6 py-3 flex justify-end">
|
||||||
|
<button @click="openModal = false"
|
||||||
|
class="px-4 py-2 bg-white/20 text-white rounded-lg hover:bg-white/30 transition-colors text-sm">
|
||||||
|
Tutup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeCharts();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
function initializeCharts() {
|
||||||
|
// Data
|
||||||
|
const positif = {{ $positif }};
|
||||||
|
const negatif = {{ $negatif }};
|
||||||
|
const scoreData = [
|
||||||
|
{{ $score_0_20 }},
|
||||||
|
{{ $score_21_40 }},
|
||||||
|
{{ $score_41_60 }},
|
||||||
|
{{ $score_61_80 }},
|
||||||
|
{{ $score_81_100 }}
|
||||||
|
];
|
||||||
|
const metricsNegatif = [
|
||||||
|
{{ $metricsNegatif['precision'] }},
|
||||||
|
{{ $metricsNegatif['recall'] }},
|
||||||
|
{{ $metricsNegatif['f1'] }}
|
||||||
|
];
|
||||||
|
const metricsPositif = [
|
||||||
|
{{ $metricsPositif['precision'] }},
|
||||||
|
{{ $metricsPositif['recall'] }},
|
||||||
|
{{ $metricsPositif['f1'] }}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Hapus chart lama
|
||||||
|
['barChart', 'pieChart', 'scoreChart', 'metricsChart'].forEach(chartId => {
|
||||||
|
const canvas = document.getElementById(chartId);
|
||||||
|
if (canvas) {
|
||||||
|
const existingChart = Chart.getChart(canvas);
|
||||||
|
if (existingChart) existingChart.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bar Chart
|
||||||
|
new Chart(document.getElementById('barChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Positif', 'Negatif'],
|
||||||
|
datasets: [{
|
||||||
|
data: [positif, negatif],
|
||||||
|
backgroundColor: ['#10b981', '#ef4444'],
|
||||||
|
borderRadius: 5,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { color: 'rgba(255,255,255,0.1)' },
|
||||||
|
ticks: { color: 'white' }
|
||||||
|
},
|
||||||
|
x: { ticks: { color: 'white' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pie Chart
|
||||||
|
new Chart(document.getElementById('pieChart'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Positif', 'Negatif'],
|
||||||
|
datasets: [{
|
||||||
|
data: [positif, negatif],
|
||||||
|
backgroundColor: ['#10b981', '#ef4444'],
|
||||||
|
borderWidth: 0,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cutout: '60%',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Score Chart
|
||||||
|
new Chart(document.getElementById('scoreChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['0-20', '21-40', '41-60', '61-80', '81-100'],
|
||||||
|
datasets: [{
|
||||||
|
data: scoreData,
|
||||||
|
borderColor: '#60a5fa',
|
||||||
|
backgroundColor: 'rgba(96, 165, 250, 0.2)',
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointBackgroundColor: '#60a5fa',
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { color: 'rgba(255,255,255,0.1)' },
|
||||||
|
ticks: { color: 'white' }
|
||||||
|
},
|
||||||
|
x: { ticks: { color: 'white' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metrics Chart
|
||||||
|
new Chart(document.getElementById('metricsChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Precision', 'Recall', 'F1-Score'],
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Negatif', data: metricsNegatif, backgroundColor: '#ef4444' },
|
||||||
|
{ label: 'Positif', data: metricsPositif, backgroundColor: '#10b981' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: { color: 'white' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
ticks: {
|
||||||
|
callback: v => (v * 100) + '%',
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
grid: { color: 'rgba(255,255,255,0.1)' }
|
||||||
|
},
|
||||||
|
x: { ticks: { color: 'white' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</x-app-layout>
|
||||||
|
|
@ -0,0 +1,418 @@
|
||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="font-black text-2xl text-white flex items-center">
|
||||||
|
<i class="fas fa-database text-white/80 mr-3 text-3xl"></i>
|
||||||
|
Manajemen Data & Dataset
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-white/70 mt-1">Kelola, upload, dan export dataset analisis sentimen</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<span class="px-3 py-1 bg-white/20 text-white rounded-full text-xs font-bold flex items-center border border-white/10">
|
||||||
|
<i class="fas fa-database mr-1"></i>
|
||||||
|
Total: {{ number_format($totalData) }} data
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-8 bg-gray-50" x-data="{
|
||||||
|
showUploadModal: false,
|
||||||
|
showDeleteModal: false,
|
||||||
|
selectedFile: null,
|
||||||
|
uploading: false,
|
||||||
|
editModal: false,
|
||||||
|
editId: null,
|
||||||
|
editReview: '',
|
||||||
|
editScore: 0,
|
||||||
|
editSentiment: 'positif'
|
||||||
|
}">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||||
|
|
||||||
|
<!-- Alert Messages -->
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded-lg shadow-md animate__animated animate__fadeInDown" role="alert">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3 text-xl"></i>
|
||||||
|
<p class="font-bold">{{ session('success') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg shadow-md animate__animated animate__fadeInDown" role="alert">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-circle text-red-500 mr-3 text-xl"></i>
|
||||||
|
<p class="font-bold">{{ session('error') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg shadow-md">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i class="fas fa-exclamation-triangle text-red-500 mr-3 text-xl mt-1"></i>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold">Terjadi kesalahan:</p>
|
||||||
|
<ul class="list-disc list-inside text-sm mt-1">
|
||||||
|
@foreach($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Quick Stats - Biru Navy -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6 border border-white/10">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-white/70">Total Dataset</p>
|
||||||
|
<p class="text-2xl font-black text-white">{{ number_format($totalData) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center">
|
||||||
|
<i class="fas fa-database text-white text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6 border border-white/10">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-white/70">Data Positif</p>
|
||||||
|
<p class="text-2xl font-black text-green-400">{{ number_format($positif) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center">
|
||||||
|
<i class="fas fa-smile text-green-400 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6 border border-white/10">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-white/70">Data Negatif</p>
|
||||||
|
<p class="text-2xl font-black text-red-400">{{ number_format($negatif) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center">
|
||||||
|
<i class="fas fa-frown text-red-400 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6 border border-white/10">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-white/70">Akurasi Model</p>
|
||||||
|
<p class="text-2xl font-black text-blue-400">{{ number_format(($metrics['accuracy'] ?? 0) * 100, 1) }}%</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center">
|
||||||
|
<i class="fas fa-chart-line text-blue-400 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Upload Card -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-2xl shadow-lg overflow-hidden">
|
||||||
|
<div class="bg-white/10 px-6 py-4 border-b border-white/10">
|
||||||
|
<h3 class="text-white font-bold flex items-center">
|
||||||
|
<i class="fas fa-upload mr-2 text-white/80"></i>
|
||||||
|
Upload Dataset
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ route('data.upload') }}" method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
<div class="border-2 border-dashed border-white/20 rounded-xl p-6 text-center hover:border-white/40 transition bg-white/5"
|
||||||
|
@dragover.prevent="active = true"
|
||||||
|
@dragleave.prevent="active = false"
|
||||||
|
@drop.prevent="active = false; $refs.file.files = $event.dataTransfer.files">
|
||||||
|
|
||||||
|
<i class="fas fa-cloud-upload-alt text-4xl text-white/40 mb-3"></i>
|
||||||
|
<p class="text-sm text-white/70 mb-2">
|
||||||
|
Drag & drop file CSV disini, atau
|
||||||
|
</p>
|
||||||
|
<label class="cursor-pointer">
|
||||||
|
<span class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg font-bold text-sm inline-block transition border border-white/10">
|
||||||
|
<i class="fas fa-folder-open mr-2"></i>Browse File
|
||||||
|
</span>
|
||||||
|
<input type="file" name="dataset" accept=".csv,.txt" class="hidden" required x-ref="file" @change="selectedFile = $event.target.files[0]">
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-white/50 mt-3">
|
||||||
|
Format: CSV dengan kolom: review, text_final, score, sentiment
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<template x-if="selectedFile">
|
||||||
|
<div class="mt-4 p-3 bg-green-500/20 rounded-lg border border-green-500/30">
|
||||||
|
<i class="fas fa-check-circle text-green-400 mr-2"></i>
|
||||||
|
<span class="text-sm text-green-300" x-text="selectedFile.name"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="flex items-center space-x-2 text-sm text-white/70">
|
||||||
|
<input type="checkbox" name="replace_data" value="1" class="rounded bg-white/20 border-white/30 text-blue-600">
|
||||||
|
<span>Ganti semua data yang ada</span>
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="bg-white/20 hover:bg-white/30 text-white px-6 py-2 rounded-lg font-bold transition transform hover:scale-105 border border-white/10">
|
||||||
|
<i class="fas fa-upload mr-2"></i>Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export & Delete Card -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-2xl shadow-lg overflow-hidden">
|
||||||
|
<div class="bg-white/10 px-6 py-4 border-b border-white/10">
|
||||||
|
<h3 class="text-white font-bold flex items-center">
|
||||||
|
<i class="fas fa-download mr-2 text-white/80"></i>
|
||||||
|
Export & Maintenance
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||||
|
<a href="{{ route('export.pdf') }}" class="bg-red-500/20 hover:bg-red-500/30 text-white p-4 rounded-xl transition transform hover:scale-105 text-center border border-red-500/30">
|
||||||
|
<i class="fas fa-file-pdf text-2xl mb-2 text-red-400"></i>
|
||||||
|
<p class="font-bold text-sm">Export PDF</p>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('export.excel') }}" class="bg-green-500/20 hover:bg-green-500/30 text-white p-4 rounded-xl transition transform hover:scale-105 text-center border border-green-500/30">
|
||||||
|
<i class="fas fa-file-excel text-2xl mb-2 text-green-400"></i>
|
||||||
|
<p class="font-bold text-sm">Export Excel</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ route('data.truncate') }}" method="POST" onsubmit="return confirm('⚠️ PERHATIAN! Anda akan menghapus SEMUA data. Tindakan ini tidak dapat dibatalkan. Lanjutkan?');">
|
||||||
|
@csrf
|
||||||
|
@method('POST')
|
||||||
|
<button type="submit" class="w-full bg-red-500/20 hover:bg-red-500/30 text-white px-4 py-3 rounded-xl font-bold transition transform hover:scale-105 flex items-center justify-center space-x-2 border border-red-500/30">
|
||||||
|
<i class="fas fa-trash-alt text-red-400"></i>
|
||||||
|
<span>Hapus Semua Data</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter & Search -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-xl shadow-lg p-6">
|
||||||
|
<form method="GET" action="{{ route('data.management') }}" class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-sm font-bold text-white/80 mb-2">
|
||||||
|
<i class="fas fa-search mr-1 text-white/60"></i>Cari Review
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text" name="search" value="{{ $search ?? '' }}" placeholder="Kata kunci review..."
|
||||||
|
class="w-full rounded-xl bg-white/10 border-white/20 pl-10 text-white placeholder-white/50 focus:border-white/30 focus:ring-white/30">
|
||||||
|
<i class="fas fa-search absolute left-3 top-3 text-white/40"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-bold text-white/80 mb-2">
|
||||||
|
<i class="fas fa-filter mr-1 text-white/60"></i>Filter Sentimen
|
||||||
|
</label>
|
||||||
|
<select name="filter_sentiment" class="w-full rounded-xl bg-white/10 border-white/20 text-white focus:border-white/30 focus:ring-white/30">
|
||||||
|
<option value="" class="bg-[#1e3a8a]">Semua</option>
|
||||||
|
<option value="positif" {{ ($filter_sentiment ?? '') == 'positif' ? 'selected' : '' }} class="bg-[#1e3a8a]">Positif</option>
|
||||||
|
<option value="negatif" {{ ($filter_sentiment ?? '') == 'negatif' ? 'selected' : '' }} class="bg-[#1e3a8a]">Negatif</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-bold text-white/80 mb-2">
|
||||||
|
<i class="fas fa-list mr-1 text-white/60"></i>Tampilkan
|
||||||
|
</label>
|
||||||
|
<select name="per_page" class="w-full rounded-xl bg-white/10 border-white/20 text-white focus:border-white/30 focus:ring-white/30">
|
||||||
|
<option value="10" {{ ($per_page ?? 10) == 10 ? 'selected' : '' }} class="bg-[#1e3a8a]">10 per halaman</option>
|
||||||
|
<option value="25" {{ ($per_page ?? 10) == 25 ? 'selected' : '' }} class="bg-[#1e3a8a]">25 per halaman</option>
|
||||||
|
<option value="50" {{ ($per_page ?? 10) == 50 ? 'selected' : '' }} class="bg-[#1e3a8a]">50 per halaman</option>
|
||||||
|
<option value="100" {{ ($per_page ?? 10) == 100 ? 'selected' : '' }} class="bg-[#1e3a8a]">100 per halaman</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-end space-x-2">
|
||||||
|
<button type="submit" class="flex-1 bg-white/20 hover:bg-white/30 text-white px-4 py-2.5 rounded-xl font-bold transition transform hover:scale-105 border border-white/10">
|
||||||
|
<i class="fas fa-search mr-2"></i>Filter
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('data.management') }}" class="flex-1 bg-white/10 hover:bg-white/20 text-white px-4 py-2.5 rounded-xl font-bold transition transform hover:scale-105 text-center border border-white/10">
|
||||||
|
<i class="fas fa-redo-alt mr-2"></i>Reset
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table -->
|
||||||
|
<div class="bg-[#1e3a8a] rounded-2xl shadow-lg overflow-hidden">
|
||||||
|
<div class="bg-white/10 px-6 py-4 flex justify-between items-center border-b border-white/10">
|
||||||
|
<h3 class="text-white font-bold flex items-center">
|
||||||
|
<i class="fas fa-table mr-2 text-white/80"></i>
|
||||||
|
Tabel Dataset
|
||||||
|
</h3>
|
||||||
|
<span class="text-white text-sm bg-white/20 px-3 py-1 rounded-full border border-white/10">
|
||||||
|
{{ $reviews->firstItem() ?? 0 }} - {{ $reviews->lastItem() ?? 0 }} dari {{ $reviews->total() }} data
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm text-left">
|
||||||
|
<thead class="bg-white/5 text-xs font-bold text-white/80 uppercase">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-4">ID</th>
|
||||||
|
<th class="px-6 py-4">Review</th>
|
||||||
|
<th class="px-6 py-4">Text Final</th>
|
||||||
|
<th class="px-6 py-4">Score</th>
|
||||||
|
<th class="px-6 py-4">Sentimen</th>
|
||||||
|
<th class="px-6 py-4">Tanggal</th>
|
||||||
|
<th class="px-6 py-4">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-white/10">
|
||||||
|
@forelse($reviews ?? [] as $review)
|
||||||
|
<tr class="hover:bg-white/5 transition">
|
||||||
|
<td class="px-6 py-4 font-mono text-sm text-white/70">#{{ $review->id ?? '-' }}</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4 max-w-xs">
|
||||||
|
<div class="flex items-center text-white">
|
||||||
|
<i class="fas fa-quote-right text-white/30 mr-2 text-xs"></i>
|
||||||
|
<span title="{{ $review->review ?? '' }}" class="truncate block">
|
||||||
|
{{ Str::limit($review->review ?? '-', 50) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4 max-w-xs text-white/60">
|
||||||
|
<span title="{{ $review->text_final ?? $review->review ?? '' }}" class="truncate block">
|
||||||
|
{{ Str::limit($review->text_final ?? $review->review ?? '-', 40) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
@php $score = $review->score ?? 0; @endphp
|
||||||
|
<span class="px-3 py-1 rounded-full text-xs font-bold
|
||||||
|
{{ $score >= 60 ? 'bg-green-500/20 text-green-300' : ($score >= 40 ? 'bg-yellow-500/20 text-yellow-300' : 'bg-red-500/20 text-red-300') }}">
|
||||||
|
{{ $score }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
@php $sentiment = $review->sentiment ?? 'netral'; @endphp
|
||||||
|
<span class="px-3 py-1 rounded-full text-xs font-bold flex items-center w-fit
|
||||||
|
{{ $sentiment == 'positif' ? 'bg-green-500/20 text-green-300' : ($sentiment == 'negatif' ? 'bg-red-500/20 text-red-300' : 'bg-gray-500/20 text-gray-300') }}">
|
||||||
|
<i class="fas fa-{{ $sentiment == 'positif' ? 'smile' : ($sentiment == 'negatif' ? 'frown' : 'meh') }} mr-1"></i>
|
||||||
|
{{ ucfirst($sentiment) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4 text-sm text-white/50">
|
||||||
|
<i class="far fa-calendar-alt mr-1"></i>
|
||||||
|
{{ $review->created_at ? $review->created_at->format('d/m/Y H:i') : date('d/m/Y H:i') }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button onclick='editReview({{ $review->id ?? 0 }}, @json($review->review ?? ""), {{ $review->score ?? 0 }}, @json($review->sentiment ?? "netral"))'
|
||||||
|
class="p-2 text-blue-400 hover:bg-white/10 rounded-lg transition" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteReview({{ $review->id ?? 0 }})"
|
||||||
|
class="p-2 text-red-400 hover:bg-white/10 rounded-lg transition" title="Delete">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="px-6 py-12 text-center text-white/50">
|
||||||
|
<i class="fas fa-database text-4xl mb-3 opacity-50"></i>
|
||||||
|
<p class="text-lg font-bold">Belum ada data tersedia</p>
|
||||||
|
<p class="text-sm mt-2">Silakan upload file CSV untuk memulai</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
@if(isset($reviews) && method_exists($reviews, 'links'))
|
||||||
|
<div class="px-6 py-4 border-t border-white/10">
|
||||||
|
{{ $reviews->appends(request()->query())->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Card -->
|
||||||
|
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/10">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<i class="fas fa-info-circle text-white/60 text-xl mt-1"></i>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-bold text-white">Informasi Dataset</p>
|
||||||
|
<p class="text-xs text-white/60 mt-1">
|
||||||
|
Dataset terdiri dari {{ number_format($totalData) }} review dengan {{ number_format($positif) }} data positif ({{ number_format(($positif/$totalSafe)*100, 1) }}%) dan {{ number_format($negatif) }} data negatif ({{ number_format(($negatif/$totalSafe)*100, 1) }}%).
|
||||||
|
Format file CSV yang diterima: review, steming_data, score (integer -9 s/d 9), sentiment (positif/negatif).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
// Format angka
|
||||||
|
function formatNumber(num) {
|
||||||
|
return new Intl.NumberFormat('id-ID').format(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto hide alert messages
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelectorAll('[role="alert"]').forEach(el => {
|
||||||
|
el.style.transition = 'opacity 0.5s';
|
||||||
|
el.style.opacity = '0';
|
||||||
|
setTimeout(() => el.remove(), 500);
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Fungsi Edit Review
|
||||||
|
function editReview(id, review, score, sentiment) {
|
||||||
|
if(confirm('Edit review?\nFitur ini akan segera tersedia')) {
|
||||||
|
// Implementasi edit akan ditambahkan kemudian
|
||||||
|
console.log('Edit review:', id, review, score, sentiment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi Delete Review
|
||||||
|
function deleteReview(id) {
|
||||||
|
if(confirm('Apakah Anda yakin ingin menghapus data ini?')) {
|
||||||
|
fetch(`/data/review/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if(data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Gagal menghapus data: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Terjadi kesalahan: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
</x-app-layout>
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{{-- resources/views/pdf/report.blade.php --}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ $title }}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||||
|
th { background-color: #f2f2f2; }
|
||||||
|
.header { text-align: center; margin-bottom: 30px; }
|
||||||
|
.stats { margin-bottom: 20px; }
|
||||||
|
.positive { color: green; }
|
||||||
|
.negative { color: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>{{ $title }}</h1>
|
||||||
|
<p>Tanggal: {{ $date }}</p>
|
||||||
|
<p>Diekspor oleh: {{ auth()->user()->name ?? 'Admin' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<h3>Statistik Sentimen</h3>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Total Data</th>
|
||||||
|
<td>{{ $totalData }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Positif</th>
|
||||||
|
<td class="positive">{{ $positif }} ({{ number_format(($positif / $totalSafe) * 100, 1) }}%)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Negatif</th>
|
||||||
|
<td class="negative">{{ $negatif }} ({{ number_format(($negatif / $totalSafe) * 100, 1) }}%)</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Data Review</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>Review</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Sentimen</th>
|
||||||
|
<th>Tanggal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($dataset as $index => $review)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $index + 1 }}</td>
|
||||||
|
<td>{{ $review->review }}</td>
|
||||||
|
<td>{{ $review->score }}</td>
|
||||||
|
<td>{{ ucfirst($review->sentiment) }}</td>
|
||||||
|
<td>{{ $review->created_at->format('d/m/Y') }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<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>
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Auth\AuthenticatedSessionController;
|
||||||
|
use App\Http\Controllers\Auth\ConfirmablePasswordController;
|
||||||
|
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
|
||||||
|
use App\Http\Controllers\Auth\EmailVerificationPromptController;
|
||||||
|
use App\Http\Controllers\Auth\NewPasswordController;
|
||||||
|
use App\Http\Controllers\Auth\PasswordController;
|
||||||
|
use App\Http\Controllers\Auth\PasswordResetLinkController;
|
||||||
|
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||||
|
use App\Http\Controllers\Auth\VerifyEmailController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::middleware('guest')->group(function () {
|
||||||
|
Route::get('register', [RegisteredUserController::class, 'create'])
|
||||||
|
->name('register');
|
||||||
|
|
||||||
|
Route::post('register', [RegisteredUserController::class, 'store']);
|
||||||
|
|
||||||
|
Route::get('login', [AuthenticatedSessionController::class, 'create'])
|
||||||
|
->name('login');
|
||||||
|
|
||||||
|
Route::post('login', [AuthenticatedSessionController::class, 'store']);
|
||||||
|
|
||||||
|
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
|
||||||
|
->name('password.request');
|
||||||
|
|
||||||
|
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
|
||||||
|
->name('password.email');
|
||||||
|
|
||||||
|
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
|
||||||
|
->name('password.reset');
|
||||||
|
|
||||||
|
Route::post('reset-password', [NewPasswordController::class, 'store'])
|
||||||
|
->name('password.store');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
|
Route::get('verify-email', EmailVerificationPromptController::class)
|
||||||
|
->name('verification.notice');
|
||||||
|
|
||||||
|
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
|
||||||
|
->middleware(['signed', 'throttle:6,1'])
|
||||||
|
->name('verification.verify');
|
||||||
|
|
||||||
|
Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
|
||||||
|
->middleware('throttle:6,1')
|
||||||
|
->name('verification.send');
|
||||||
|
|
||||||
|
Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
|
||||||
|
->name('password.confirm');
|
||||||
|
|
||||||
|
Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
|
||||||
|
|
||||||
|
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
|
||||||
|
|
||||||
|
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
|
||||||
|
->name('logout');
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Inspiring;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
|
Artisan::command('inspire', function () {
|
||||||
|
$this->comment(Inspiring::quote());
|
||||||
|
})->purpose('Display an inspiring quote');
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue