login register forgot update done
This commit is contained in:
parent
fe67b688d2
commit
e1f990c895
|
|
@ -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,58 @@
|
|||
<?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([
|
||||
'username' => ['required', 'string', 'max:255', 'unique:users', 'regex:/^[a-zA-Z0-9_]+$/'],
|
||||
'nama' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'no_hp' => ['required', 'string', 'max:15', 'regex:/^(\+62|62|0)[0-9]{9,13}$/'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
],[
|
||||
'username.regex' => 'Username hanya boleh huruf, angka, dan underscore',
|
||||
'no_hp.regex' => 'Format nomor HP tidak valid. Contoh: 08123456789',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'username' => $request->username,
|
||||
'nama' => $request->nama,
|
||||
'email' => $request->email,
|
||||
'no_hp' => $request->no_hp,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'user', //default role untuk user baru
|
||||
]);
|
||||
|
||||
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,64 @@
|
|||
<?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
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
// Update data user
|
||||
$request->user()->fill($validated);
|
||||
|
||||
// Reset email verification jika email berubah
|
||||
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,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,46 @@
|
|||
<?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 [
|
||||
'username' => ['required', 'string', 'max:255', 'regex:/^[a-zA-Z0-9_]+$/', Rule::unique(User::class)->ignore($this->user()->id)],
|
||||
'nama' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
'no_hp' => ['required', 'string', 'max:15', 'regex:/^(\+62|62|0)[0-9]{9,13}$/'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'username.regex' => 'Username hanya boleh huruf, angka, dan underscore',
|
||||
'username.unique' => 'Username sudah digunakan',
|
||||
'no_hp.regex' => 'Format nomor HP tidak valid. Contoh: 08123456789',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InformasiBudidaya extends Model
|
||||
{
|
||||
protected $table = 'informasi_budidaya';
|
||||
|
||||
protected $fillable = [
|
||||
'judul',
|
||||
'slug',
|
||||
'deskripsi_singkat',
|
||||
'konten',
|
||||
'gambar_utama',
|
||||
'galeri_gambar',
|
||||
'file_pdf',
|
||||
'kategori',
|
||||
'tags',
|
||||
'urutan',
|
||||
'is_published',
|
||||
'published_at',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'galeri_gambar' => 'array',
|
||||
'tags' => 'array',
|
||||
'is_published' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function author()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('is_published', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InformasiHamaPenyakit extends Model
|
||||
{
|
||||
protected $table = 'informasi_hama_penyakit';
|
||||
|
||||
protected $fillable = [
|
||||
'id_penyakit',
|
||||
'judul',
|
||||
'slug',
|
||||
'deskripsi_singkat',
|
||||
'konten',
|
||||
'gambar_utama',
|
||||
'galeri_gambar',
|
||||
'file_pdf',
|
||||
'jenis',
|
||||
'tags',
|
||||
'gejala_visual',
|
||||
'cara_identifikasi',
|
||||
'pencegahan',
|
||||
'pengendalian',
|
||||
'urutan',
|
||||
'is_published',
|
||||
'published_at',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'galeri_gambar' => 'array',
|
||||
'tags' => 'array',
|
||||
'is_published' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function author()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function penyakit()
|
||||
{
|
||||
return $this->belongsTo(MasterPenyakit::class, 'id_penyakit', 'id_penyakit');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('is_published', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MasterGejala extends Model
|
||||
{
|
||||
protected $table = 'master_gejala';
|
||||
protected $primaryKey = 'id_gejala';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'id_gejala',
|
||||
'nama_gejala',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function ruleBasis()
|
||||
{
|
||||
return $this->hasMany(RuleBasis::class, 'id_gejala', 'id_gejala');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MasterPenyakit extends Model
|
||||
{
|
||||
protected $table = 'master_penyakit';
|
||||
protected $primaryKey = 'id_penyakit';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'id_penyakit',
|
||||
'nama_penyakit',
|
||||
'nama_latin',
|
||||
'kategori',
|
||||
'deskripsi_singkat',
|
||||
'deskripsi_lengkap',
|
||||
'pengendalian_pencegahan',
|
||||
'pengendalian_kimia',
|
||||
'pengendalian_organik',
|
||||
'pengendalian_budidaya',
|
||||
'tingkat_bahaya',
|
||||
'gambar_url',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function ruleBasis()
|
||||
{
|
||||
return $this->hasMany(RuleBasis::class, 'id_penyakit', 'id_penyakit');
|
||||
}
|
||||
|
||||
public function riwayatDiagnosis()
|
||||
{
|
||||
return $this->hasMany(RiwayatDiagnosis::class, 'penyakit_final', 'id_penyakit');
|
||||
}
|
||||
|
||||
public function informasiHamaPenyakit()
|
||||
{
|
||||
return $this->hasMany(InformasiHamaPenyakit::class, 'id_penyakit', 'id_penyakit');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RiwayatDiagnosis extends Model
|
||||
{
|
||||
protected $table = 'riwayat_diagnosis';
|
||||
protected $primaryKey = 'id_diagnosis';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'tanggal',
|
||||
'gejala_input',
|
||||
'hasil_diagnosa',
|
||||
'cf_tertinggi',
|
||||
'penyakit_final',
|
||||
'user_info',
|
||||
'lokasi',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'gejala_input' => 'array',
|
||||
'hasil_diagnosa' => 'array',
|
||||
'tanggal' => 'datetime',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function penyakit()
|
||||
{
|
||||
return $this->belongsTo(MasterPenyakit::class, 'penyakit_final', 'id_penyakit');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RuleBasis extends Model
|
||||
{
|
||||
protected $table = 'rule_basis';
|
||||
protected $primaryKey = 'id_rule';
|
||||
|
||||
protected $fillable = [
|
||||
'id_penyakit',
|
||||
'id_gejala',
|
||||
'mb', // ← TAMBAH
|
||||
'md', // ← TAMBAH
|
||||
'cf_pakar',
|
||||
'keterangan',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function penyakit()
|
||||
{
|
||||
return $this->belongsTo(MasterPenyakit::class, 'id_penyakit', 'id_penyakit');
|
||||
}
|
||||
|
||||
public function gejala()
|
||||
{
|
||||
return $this->belongsTo(MasterGejala::class, 'id_gejala', 'id_gejala');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// AUTO-CALCULATE CF dari MB & MD
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Otomatis hitung CF sebelum save
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Event: Sebelum create
|
||||
static::creating(function ($rule) {
|
||||
$rule->cf_pakar = $rule->mb - $rule->md;
|
||||
});
|
||||
|
||||
// Event: Sebelum update
|
||||
static::updating(function ($rule) {
|
||||
if ($rule->isDirty(['mb', 'md'])) {
|
||||
$rule->cf_pakar = $rule->mb - $rule->md;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor: Hitung CF on-the-fly (alternatif)
|
||||
*/
|
||||
public function getCfAttribute()
|
||||
{
|
||||
return $this->mb - $this->md;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,40 +9,90 @@
|
|||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
* Kolom yang bisa diisi mass assignment
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'username',
|
||||
'nama',
|
||||
'email',
|
||||
'no_hp',
|
||||
'role',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
* Kolom yang di-hidden saat serialization (JSON)
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
* Casting tipe data
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'password' => 'hashed', // Laravel 10+ auto hash
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// RELATIONSHIPS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* User punya banyak riwayat diagnosis
|
||||
*/
|
||||
public function riwayatDiagnosis()
|
||||
{
|
||||
return $this->hasMany(RiwayatDiagnosis::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin upload banyak artikel budidaya
|
||||
*/
|
||||
public function artikelBudidaya()
|
||||
{
|
||||
return $this->hasMany(InformasiBudidaya::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin upload banyak artikel hama/penyakit
|
||||
*/
|
||||
public function artikelHamaPenyakit()
|
||||
{
|
||||
return $this->hasMany(InformasiHamaPenyakit::class, 'created_by');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPER METHODS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Cek apakah user adalah admin
|
||||
*/
|
||||
public function isAdmin()
|
||||
{
|
||||
return $this->role === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah user biasa
|
||||
*/
|
||||
public function isUser()
|
||||
{
|
||||
return $this->role === 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name
|
||||
* (Karena Laravel default cari 'name', kita redirect ke 'nama')
|
||||
*/
|
||||
public function getNameAttribute()
|
||||
{
|
||||
return $this->nama;
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/breeze": "^2.3",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c514d8f7b9fc5970bdd94287905ef584",
|
||||
"content-hash": "bdc6d92507d397adf1c269ed49ec424d",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
|
@ -6191,6 +6191,67 @@
|
|||
},
|
||||
"time": "2025-04-30T06:54:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/breeze",
|
||||
"version": "v2.3.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/breeze.git",
|
||||
"reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/breeze/zipball/1a29c5792818bd4cddf70b5f743a227e02fbcfcd",
|
||||
"reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/console": "^11.0|^12.0",
|
||||
"illuminate/filesystem": "^11.0|^12.0",
|
||||
"illuminate/support": "^11.0|^12.0",
|
||||
"illuminate/validation": "^11.0|^12.0",
|
||||
"php": "^8.2.0",
|
||||
"symfony/console": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/framework": "^11.0|^12.0",
|
||||
"orchestra/testbench-core": "^9.0|^10.0",
|
||||
"phpstan/phpstan": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Breeze\\BreezeServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Breeze\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.",
|
||||
"keywords": [
|
||||
"auth",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/breeze/issues",
|
||||
"source": "https://github.com/laravel/breeze"
|
||||
},
|
||||
"time": "2025-07-18T18:49:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pail",
|
||||
"version": "v1.2.5",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<?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::table('rule_basis', function (Blueprint $table) {
|
||||
$table->decimal('mb', 3, 2)->after('id_gejala')->comment('Measure of Belief');
|
||||
$table->decimal('md', 3, 2)->after('mb')->comment('Measure of Disbelief');
|
||||
|
||||
// cf_pakar jadi hasil kalkulasi otomatis (MB - MD)
|
||||
// Tapi tetap disimpan untuk performa query
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('rule_basis', function (Blueprint $table) {
|
||||
$table->dropColumn(['mb', 'md']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?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::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'name',
|
||||
'email_verified_at',
|
||||
'remember_token',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('name')->nullable();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->rememberToken();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
// Cek apakah kolom sudah ada, kalau belum baru tambah
|
||||
|
||||
if (!Schema::hasColumn('users', 'email_verified_at')) {
|
||||
$table->timestamp('email_verified_at')->nullable()->after('email');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'remember_token')) {
|
||||
$table->rememberToken()->after('password');
|
||||
}
|
||||
});
|
||||
|
||||
// Set email_verified_at untuk user yang sudah ada
|
||||
DB::table('users')
|
||||
->whereNull('email_verified_at')
|
||||
->update(['email_verified_at' => now()]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'email_verified_at')) {
|
||||
$table->dropColumn('email_verified_at');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('users', 'remember_token')) {
|
||||
$table->dropColumn('remember_token');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -2,24 +2,20 @@
|
|||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
// URUTAN PENTING! Parent dulu, Child terakhir
|
||||
$this->call([
|
||||
UserSeeder::class, // ← Independen (boleh duluan)
|
||||
MasterPenyakitSeeder::class, // ← Parent 1 (harus duluan)
|
||||
MasterGejalaSeeder::class, // ← Parent 2 (harus duluan)
|
||||
RuleBasisSeeder::class, // ← Child (terakhir, setelah parent ada)
|
||||
// InformasiBudidayaSeeder::class, // ← Kalau sudah ada
|
||||
// InformasiHamaPenyakitSeeder::class, // ← Kalau sudah ada
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\MasterGejala;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MasterGejalaSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
MasterGejala::truncate();
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$data = [
|
||||
// HP001 - Penggerek Buah Kopi
|
||||
['id_gejala' => 'G001', 'nama_gejala' => 'Terdapat kotoran bekas gerekan di sekitar lubang masuk pada ujung bawah buah'],
|
||||
['id_gejala' => 'G002', 'nama_gejala' => 'Pada buah muda: buah tidak berkembang, warna buah berubah menjadi kuning kemerahan dan buah gugur'],
|
||||
['id_gejala' => 'G003', 'nama_gejala' => 'Pada buah tua: biji berlubang, biji kopi cacat dan penurunan mutu biji'],
|
||||
|
||||
// HP002 - Penggerek Batang Merah
|
||||
['id_gejala' => 'G004', 'nama_gejala' => 'Terdapat lubang masuk larva di permukaan kulit batang atau cabang dengan diameter sekitar 2 mm'],
|
||||
['id_gejala' => 'G005', 'nama_gejala' => 'Terdapat serbuk gerek berbentuk bulatan kecil berdiameter 1–2 mm dengan warna cokelat kemerahan yang terkumpul di bawah pohon jika larva masih aktif'],
|
||||
['id_gejala' => 'G006', 'nama_gejala' => 'Daun tanaman layu kemudian rontok, tanaman menjadi kering, dan akhirnya mati'],
|
||||
['id_gejala' => 'G007', 'nama_gejala' => 'Bagian tanaman di atas gerekan mengering, mati, dan mudah patah apabila luas gerekan melingkar dan bertemu'],
|
||||
|
||||
// HP003 - Penggerek Cabang
|
||||
['id_gejala' => 'G008', 'nama_gejala' => 'Terdapat lubang gerekan pada cabang dan ranting dengan diameter 1-2 mm'],
|
||||
['id_gejala' => 'G009', 'nama_gejala' => 'Cabang dan ranting yang terserang menjadi rapuh dan mudah patah'],
|
||||
|
||||
// HP004 - Kutu Hijau
|
||||
['id_gejala' => 'G010', 'nama_gejala' => 'Daun menguning dan mengering pada bagian ujung cabang'],
|
||||
['id_gejala' => 'G011', 'nama_gejala' => 'Terdapat koloni kutu berwarna hijau pada permukaan bawah daun'],
|
||||
['id_gejala' => 'G012', 'nama_gejala' => 'Permukaan bawah daun ditumbuhi jamur embun jelaga berwarna hitam'],
|
||||
['id_gejala' => 'G013', 'nama_gejala' => 'Daun dan batang muda ditutupi embun jelaga sehingga mengganggu proses fotosintesis'],
|
||||
|
||||
// HP005 - Wereng
|
||||
['id_gejala' => 'G014', 'nama_gejala' => 'Daun berlubang kecil akibat tusukan wereng'],
|
||||
['id_gejala' => 'G015', 'nama_gejala' => 'Terdapat bercak-bercak kuning pada daun'],
|
||||
['id_gejala' => 'G016', 'nama_gejala' => 'Pertumbuhan tanaman terhambat, tunas cacat bentuknya, rontok, atau mati'],
|
||||
['id_gejala' => 'G017', 'nama_gejala' => 'Lapisan lilin yang dihasilkan wereng ditumbuhi jamur jelaga sehingga daun susah berfotosintesis'],
|
||||
|
||||
// HP006 - Karat Daun
|
||||
['id_gejala' => 'G018', 'nama_gejala' => 'Bercak kuning pucat pada permukaan atas daun'],
|
||||
['id_gejala' => 'G019', 'nama_gejala' => 'Bercak berkembang menjadi spora berwarna kuning-oranye seperti karat pada permukaan bawah daun'],
|
||||
['id_gejala' => 'G020', 'nama_gejala' => 'Daun gugur prematur dan tanaman menjadi gundul'],
|
||||
|
||||
// HP007 - Bercak Daun
|
||||
['id_gejala' => 'G021', 'nama_gejala' => 'Bercak coklat dengan tepi kuning pada daun'],
|
||||
['id_gejala' => 'G022', 'nama_gejala' => 'Bercak meluas dan bergabung membentuk area nekrotik yang luas'],
|
||||
['id_gejala' => 'G023', 'nama_gejala' => 'Daun gugur dan tanaman melemah'],
|
||||
|
||||
// HP008 - Jamur Upas
|
||||
['id_gejala' => 'G024', 'nama_gejala' => 'Benang-benang jamur berwarna jingga atau merah muda pada batang dan cabang'],
|
||||
['id_gejala' => 'G025', 'nama_gejala' => 'Kulit batang terkelupas dan menampakkan kayu di bawahnya'],
|
||||
['id_gejala' => 'G026', 'nama_gejala' => 'Bagian tanaman di atas infeksi mati perlahan'],
|
||||
|
||||
// HP009 - Kanker Belah
|
||||
['id_gejala' => 'G027', 'nama_gejala' => 'Batang pecah atau belah secara vertikal'],
|
||||
['id_gejala' => 'G028', 'nama_gejala' => 'Terdapat gusi atau getah yang keluar dari luka'],
|
||||
['id_gejala' => 'G029', 'nama_gejala' => 'Miselium jamur putih terlihat pada kayu yang terinfeksi'],
|
||||
['id_gejala' => 'G030', 'nama_gejala' => 'Akar tanaman membusuk dengan miselium jamur putih'],
|
||||
|
||||
// HP010 - Jamur Akar Putih
|
||||
['id_gejala' => 'G031', 'nama_gejala' => 'Tanaman layu mendadak meskipun air cukup'],
|
||||
['id_gejala' => 'G032', 'nama_gejala' => 'Akar tertutup miselium jamur berwarna putih seperti benang'],
|
||||
['id_gejala' => 'G033', 'nama_gejala' => 'Kulit akar mudah terkelupas dan menampakkan kayu yang membusuk'],
|
||||
['id_gejala' => 'G034', 'nama_gejala' => 'Tanaman mati dalam waktu singkat setelah gejala muncul'],
|
||||
|
||||
// HP012 - Jamur Akar Coklat
|
||||
['id_gejala' => 'G035', 'nama_gejala' => 'Daun-daun menguning, layu, dan akhirnya gugur serta cabang-cabang mati'],
|
||||
['id_gejala' => 'G036', 'nama_gejala' => 'Akar tertutup miselium jamur berwarna coklat'],
|
||||
['id_gejala' => 'G037', 'nama_gejala' => 'Akar membusuk dan rapuh'],
|
||||
['id_gejala' => 'G038', 'nama_gejala' => 'Tanaman mudah roboh karena sistem perakaran rusak'],
|
||||
|
||||
// HP013 - Jamur Akar Hitam
|
||||
['id_gejala' => 'G039', 'nama_gejala' => 'Pertumbuhan tanaman terhambat dan kerdil'],
|
||||
['id_gejala' => 'G040', 'nama_gejala' => 'Daun menguning dan gugur tidak normal'],
|
||||
['id_gejala' => 'G041', 'nama_gejala' => 'Akar tertutup miselium jamur berwarna hitam'],
|
||||
['id_gejala' => 'G042', 'nama_gejala' => 'Akar membusuk dengan bau busuk yang khas'],
|
||||
|
||||
// HP014 - Mati Pucuk
|
||||
['id_gejala' => 'G043', 'nama_gejala' => 'Pucuk tanaman mati dan mengering'],
|
||||
['id_gejala' => 'G044', 'nama_gejala' => 'Daun muda berwarna coklat kehitaman'],
|
||||
['id_gejala' => 'G045', 'nama_gejala' => 'Batang muda menghitam dan membusuk dari ujung'],
|
||||
['id_gejala' => 'G046', 'nama_gejala' => 'Bunga dan buah muda gugur'],
|
||||
['id_gejala' => 'G047', 'nama_gejala' => 'Cabang primer mati mundur dari ujung'],
|
||||
['id_gejala' => 'G048', 'nama_gejala' => 'Pada batang: Batang pecah atau belah'],
|
||||
['id_gejala' => 'G049', 'nama_gejala' => 'Pada batang: Luka terbuka pada kulit batang'],
|
||||
['id_gejala' => 'G050', 'nama_gejala' => 'Pada pohon muda: Pertumbuhan batang terhambat'],
|
||||
['id_gejala' => 'G051', 'nama_gejala' => 'Pada pohon muda: Tanaman kerdil atau tidak tumbuh normal'],
|
||||
['id_gejala' => 'G052', 'nama_gejala' => 'Pada pohon dewasa: Cabang-cabang mati bertahap'],
|
||||
['id_gejala' => 'G053', 'nama_gejala' => 'Pada pohon dewasa: Daun menguning dan rontok tidak merata'],
|
||||
|
||||
// HP015 - Rebah Batang
|
||||
['id_gejala' => 'G054', 'nama_gejala' => 'Benih atau bibit membusuk sebelum tumbuh'],
|
||||
['id_gejala' => 'G055', 'nama_gejala' => 'Batang bibit muda terlihat basah dan berair'],
|
||||
['id_gejala' => 'G056', 'nama_gejala' => 'Batang mengecil dan melemah di dekat permukaan tanah'],
|
||||
['id_gejala' => 'G057', 'nama_gejala' => 'Tanaman rebah atau roboh karena batang tidak kuat menopang'],
|
||||
['id_gejala' => 'G058', 'nama_gejala' => 'Akar membusuk berwarna coklat kehitaman'],
|
||||
|
||||
// HP016 - Nematoda
|
||||
['id_gejala' => 'G059', 'nama_gejala' => 'Akar bengkak atau membentuk puru-puru (galls)'],
|
||||
['id_gejala' => 'G060', 'nama_gejala' => 'Pertumbuhan tanaman terhambat'],
|
||||
['id_gejala' => 'G061', 'nama_gejala' => 'Daun menguning, layu, dan gugur'],
|
||||
['id_gejala' => 'G062', 'nama_gejala' => 'Cabang-cabang samping tidak tumbuh dengan baik'],
|
||||
['id_gejala' => 'G063', 'nama_gejala' => 'Tanaman mudah kekurangan air meskipun disiram cukup'],
|
||||
['id_gejala' => 'G064', 'nama_gejala' => 'Sistem perakaran dangkal dan tidak berkembang'],
|
||||
];
|
||||
|
||||
foreach ($data as $item) {
|
||||
MasterGejala::create($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\MasterPenyakit;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MasterPenyakitSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// ============================================
|
||||
// STEP 1: TRUNCATE (Hapus data lama)
|
||||
// ============================================
|
||||
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;'); // Nonaktifkan foreign key check
|
||||
MasterPenyakit::truncate(); // Hapus semua data
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;'); // Aktifkan kembali
|
||||
|
||||
// ============================================
|
||||
// STEP 2: INSERT data baru
|
||||
// ============================================
|
||||
|
||||
$data = [
|
||||
[
|
||||
'id_penyakit' => 'HP001',
|
||||
'nama_penyakit' => 'Penggerek Buah Kopi',
|
||||
'nama_latin' => 'Hypothenemus hampei',
|
||||
'kategori' => 'Hama',
|
||||
'deskripsi_singkat' => 'Hama PBKo menyerang semua jenis kopi dengan menggerek buah.',
|
||||
'tingkat_bahaya' => 'Sangat Tinggi',
|
||||
],
|
||||
[
|
||||
'id_penyakit' => 'HP002',
|
||||
'nama_penyakit' => 'Penggerek Batang Merah',
|
||||
'nama_latin' => 'Zeuzera coffeae',
|
||||
'kategori' => 'Hama',
|
||||
'deskripsi_singkat' => 'Larva menggerek batang kopi dari dalam.',
|
||||
'tingkat_bahaya' => 'Tinggi',
|
||||
],
|
||||
// ... tambahkan semua data
|
||||
];
|
||||
|
||||
foreach ($data as $item) {
|
||||
MasterPenyakit::create($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\RuleBasis;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RuleBasisSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Truncate
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
RuleBasis::truncate();
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
// Data dengan MB & MD
|
||||
$data = [
|
||||
// HP001 - Penggerek Buah Kopi
|
||||
[
|
||||
'id_penyakit' => 'HP001',
|
||||
'id_gejala' => 'G001',
|
||||
'mb' => 0.95, // ← TAMBAH INI
|
||||
'md' => 0.05, // ← TAMBAH INI
|
||||
// cf_pakar akan otomatis = 0.95 - 0.05 = 0.90
|
||||
'keterangan' => 'Gejala sangat khas',
|
||||
],
|
||||
[
|
||||
'id_penyakit' => 'HP001',
|
||||
'id_gejala' => 'G002',
|
||||
'mb' => 0.75, // ← TAMBAH INI
|
||||
'md' => 0.05, // ← TAMBAH INI
|
||||
// cf_pakar = 0.75 - 0.05 = 0.70
|
||||
'keterangan' => 'Gejala khas',
|
||||
],
|
||||
[
|
||||
'id_penyakit' => 'HP001',
|
||||
'id_gejala' => 'G003',
|
||||
'mb' => 0.90, // ← TAMBAH INI
|
||||
'md' => 0.05, // ← TAMBAH INI
|
||||
// cf_pakar = 0.90 - 0.05 = 0.85
|
||||
'keterangan' => 'Gejala sangat khas',
|
||||
],
|
||||
|
||||
// HP002 - Penggerek Batang Merah
|
||||
[
|
||||
'id_penyakit' => 'HP002',
|
||||
'id_gejala' => 'G004',
|
||||
'mb' => 0.85,
|
||||
'md' => 0.05,
|
||||
'keterangan' => 'Gejala khas',
|
||||
],
|
||||
[
|
||||
'id_penyakit' => 'HP002',
|
||||
'id_gejala' => 'G005',
|
||||
'mb' => 0.80,
|
||||
'md' => 0.05,
|
||||
'keterangan' => 'Gejala khas',
|
||||
],
|
||||
|
||||
// ... tambahkan semua rule dengan MB & MD
|
||||
];
|
||||
|
||||
foreach ($data as $item) {
|
||||
RuleBasis::create($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Truncate data lama
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
User::truncate();
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
// Data baru
|
||||
$users = [
|
||||
[
|
||||
'username' => 'admin',
|
||||
'nama' => 'Administrator',
|
||||
'email' => 'admin@budidayakopi.com',
|
||||
'no_hp' => '081234567890',
|
||||
'role' => 'admin',
|
||||
'password' => Hash::make('password'),
|
||||
],
|
||||
[
|
||||
'username' => 'petani_andi',
|
||||
'nama' => 'Andi Saputra',
|
||||
'email' => 'andi@gmail.com',
|
||||
'no_hp' => '081987654321',
|
||||
'role' => 'user',
|
||||
'password' => Hash::make('password'),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($users as $user) {
|
||||
User::create($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,11 +7,15 @@
|
|||
"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",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"vite": "^7.0.7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
|
|
@ -1,11 +1,3 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
|
|||
|
|
@ -1 +1,7 @@
|
|||
import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
|
|
|
|||
|
|
@ -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,269 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Forgot Password - Sistem Pakar Tanaman Kopi</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #5FA357 0%, #23753E 100%);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* LEFT SIDE - IMAGE */
|
||||
.image-section {
|
||||
flex: 1;
|
||||
background-image: url('/asset/images/jenis-tanaman-kopi-di-indonesia.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(95, 163, 87, 0.3) 0%, rgba(35, 117, 62, 0.4) 100%);
|
||||
}
|
||||
|
||||
.image-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
.image-content h2 {
|
||||
color: white;
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* RIGHT SIDE - FORM */
|
||||
.form-section {
|
||||
flex: 1;
|
||||
padding: 80px 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #23753E;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #5FA357;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: #23753E;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #5FA357;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(95, 163, 87, 0.4);
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-footer a {
|
||||
color: #5FA357;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.form-footer a:hover {
|
||||
color: #23753E;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e74c3c;
|
||||
font-size: 13px;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.session-status {
|
||||
background: #C1FA70;
|
||||
color: #23753E;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 25px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #f0f9ff;
|
||||
border-left: 4px solid #5FA357;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 968px) {
|
||||
.container {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.image-content h2 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- LEFT: IMAGE SECTION -->
|
||||
<div class="image-section">
|
||||
<div class="image-content">
|
||||
<h2>SISTEM PAKAR<br>TANAMAN KOPI</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: FORM SECTION -->
|
||||
<div class="form-section">
|
||||
<div class="form-header">
|
||||
<h1>FORGOT PASSWORD</h1>
|
||||
<p>Forgot your password? No problem. Just let us know your email address and we will email you a password reset link.</p>
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
@if (session('status'))
|
||||
<div class="session-status">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email') }}" required autofocus>
|
||||
@error('email')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" class="submit-btn">EMAIL PASSWORD RESET LINK</button>
|
||||
|
||||
<!-- Back to Login -->
|
||||
<div class="form-footer">
|
||||
Remember your password? <a href="{{ route('login') }}">Back to Login</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Sistem Pakar Tanaman Kopi</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #5FA357 0%, #23753E 100%);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* LEFT SIDE - IMAGE */
|
||||
.image-section {
|
||||
flex: 1;
|
||||
background-image: url('/asset/images/kopi4.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(95, 163, 87, 0.3) 0%, rgba(35, 117, 62, 0.4) 100%);
|
||||
}
|
||||
|
||||
.image-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
.image-content h2 {
|
||||
color: white;
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* RIGHT SIDE - FORM */
|
||||
.form-section {
|
||||
flex: 1;
|
||||
padding: 80px 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #23753E;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #5FA357;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.password-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-password {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: color 0.3s;
|
||||
user-select: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toggle-password:hover {
|
||||
color: #5FA357;
|
||||
}
|
||||
|
||||
.toggle-password svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
text-align: right;
|
||||
margin-top: -15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.forgot-password a {
|
||||
color: #5FA357;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.forgot-password a:hover {
|
||||
color: #23753E;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 25px 0 35px 0;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: #5FA357;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: #23753E;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #5FA357;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(95, 163, 87, 0.4);
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-footer a {
|
||||
color: #5FA357;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.form-footer a:hover {
|
||||
color: #23753E;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e74c3c;
|
||||
font-size: 13px;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.session-status {
|
||||
background: #C1FA70;
|
||||
color: #23753E;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 25px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 968px) {
|
||||
.container {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.image-content h2 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- LEFT: IMAGE SECTION -->
|
||||
<div class="image-section">
|
||||
<div class="image-content">
|
||||
<h2>SISTEM PAKAR<br>TANAMAN KOPI</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: FORM SECTION -->
|
||||
<div class="form-section">
|
||||
<div class="form-header">
|
||||
<h1>LOGIN</h1>
|
||||
<p>Login to your account for more information</p>
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
@if (session('status'))
|
||||
<div class="session-status">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email') }}" required autofocus autocomplete="username">
|
||||
@error('email')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
<span class="toggle-password" onclick="togglePassword('password')">
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
@error('password')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Forgot Password -->
|
||||
<div class="forgot-password">
|
||||
@if (Route::has('password.request'))
|
||||
<a href="{{ route('password.request') }}">Forgot your password?</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="remember_me" name="remember">
|
||||
<label for="remember_me">Remember me</label>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" class="submit-btn">LOGIN</button>
|
||||
|
||||
<!-- Register Link -->
|
||||
<div class="form-footer">
|
||||
Don't you have an account? <a href="{{ route('register') }}">Register</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle password visibility
|
||||
function togglePassword(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const wrapper = input.parentElement;
|
||||
const toggleBtn = wrapper.querySelector('.toggle-password');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
// Change to eye-slash icon
|
||||
toggleBtn.innerHTML = `
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
</svg>
|
||||
`;
|
||||
} else {
|
||||
input.type = 'password';
|
||||
// Change back to eye icon
|
||||
toggleBtn.innerHTML = `
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,386 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register - Sistem Pakar Tanaman Kopi</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #5FA357 0%, #23753E 100%);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* LEFT SIDE - FORM */
|
||||
.form-section {
|
||||
flex: 1;
|
||||
padding: 60px 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #23753E;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.form-header p a {
|
||||
color: #5FA357;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.form-header p a:hover {
|
||||
color: #23753E;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #5FA357;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.password-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-password {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: color 0.3s;
|
||||
user-select: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toggle-password:hover {
|
||||
color: #5FA357;
|
||||
}
|
||||
|
||||
.toggle-password svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 25px 0 30px 0;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
accent-color: #5FA357;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: #23753E;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background: #5FA357;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(95, 163, 87, 0.4);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e74c3c;
|
||||
font-size: 13px;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* RIGHT SIDE - IMAGE */
|
||||
.image-section {
|
||||
flex: 1;
|
||||
background-image: url('/asset/images/kopi3.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(95, 163, 87, 0.3) 0%, rgba(35, 117, 62, 0.4) 100%);
|
||||
}
|
||||
|
||||
.image-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
.image-content h2 {
|
||||
color: white;
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 968px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.image-content h2 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- LEFT: FORM SECTION -->
|
||||
<div class="form-section">
|
||||
<div class="form-header">
|
||||
<h1>REGISTER</h1>
|
||||
<p>Do you have already an account? <a href="{{ route('login') }}">Login</a></p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('register') }}" id="registerForm">
|
||||
@csrf
|
||||
|
||||
<!-- Row 1: Nama & Username -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="nama">Nama</label>
|
||||
<input type="text" id="nama" name="nama" value="{{ old('nama') }}" required autofocus>
|
||||
@error('nama')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" value="{{ old('username') }}" required>
|
||||
@error('username')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email') }}" required>
|
||||
@error('email')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Nomor HP -->
|
||||
<div class="form-group">
|
||||
<label for="no_hp">Nomor HP</label>
|
||||
<input type="text" id="no_hp" name="no_hp" value="{{ old('no_hp') }}" placeholder="08xxxxxxxxxx" required>
|
||||
@error('no_hp')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Password & Confirm Password -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="password" name="password" required>
|
||||
<span class="toggle-password" onclick="togglePassword('password')">
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
@error('password')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password_confirmation">Confirm Password</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="password_confirmation" name="password_confirmation" required>
|
||||
<span class="toggle-password" onclick="togglePassword('password_confirmation')">
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox Agreement -->
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="agree" name="agree" onchange="toggleSubmitButton()">
|
||||
<label for="agree">I Agree to the Terms & Conditions</label>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" class="submit-btn" id="submitBtn" disabled>REGISTER</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: IMAGE SECTION -->
|
||||
<div class="image-section">
|
||||
<div class="image-content">
|
||||
<h2>SISTEM PAKAR<br>TANAMAN KOPI</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle password visibility
|
||||
function togglePassword(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const wrapper = input.parentElement;
|
||||
const toggleBtn = wrapper.querySelector('.toggle-password');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
// Change to eye-slash icon
|
||||
toggleBtn.innerHTML = `
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
</svg>
|
||||
`;
|
||||
} else {
|
||||
input.type = 'password';
|
||||
// Change back to eye icon
|
||||
toggleBtn.innerHTML = `
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Enable/disable submit button based on checkbox
|
||||
function toggleSubmitButton() {
|
||||
const checkbox = document.getElementById('agree');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = !checkbox.checked;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reset Password - Sistem Pakar Tanaman Kopi</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #5FA357 0%, #23753E 100%);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* LEFT SIDE - IMAGE */
|
||||
.image-section {
|
||||
flex: 1;
|
||||
background-image: url('/asset/images/jenis-tanaman-kopi-di-indonesia.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(95, 163, 87, 0.3) 0%, rgba(35, 117, 62, 0.4) 100%);
|
||||
}
|
||||
|
||||
.image-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
.image-content h2 {
|
||||
color: white;
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* RIGHT SIDE - FORM */
|
||||
.form-section {
|
||||
flex: 1;
|
||||
padding: 80px 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #23753E;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #5FA357;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.password-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-password {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: color 0.3s;
|
||||
user-select: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toggle-password:hover {
|
||||
color: #5FA357;
|
||||
}
|
||||
|
||||
.toggle-password svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: #23753E;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #5FA357;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(95, 163, 87, 0.4);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e74c3c;
|
||||
font-size: 13px;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 968px) {
|
||||
.container {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.image-content h2 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- LEFT: IMAGE SECTION -->
|
||||
<div class="image-section">
|
||||
<div class="image-content">
|
||||
<h2>SISTEM PAKAR<br>TANAMAN KOPI</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: FORM SECTION -->
|
||||
<div class="form-section">
|
||||
<div class="form-header">
|
||||
<h1>RESET PASSWORD</h1>
|
||||
<p>Enter your new password below</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('password.store') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Password Reset Token -->
|
||||
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
|
||||
<!-- Email Address -->
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email', $request->email) }}" required autofocus autocomplete="username">
|
||||
@error('email')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="form-group">
|
||||
<label for="password">New Password</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="password" name="password" required autocomplete="new-password">
|
||||
<span class="toggle-password" onclick="togglePassword('password')">
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
@error('password')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="form-group">
|
||||
<label for="password_confirmation">Confirm Password</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="password_confirmation" name="password_confirmation" required autocomplete="new-password">
|
||||
<span class="toggle-password" onclick="togglePassword('password_confirmation')">
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
@error('password_confirmation')
|
||||
<span class="error-message show">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" class="submit-btn">RESET PASSWORD</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle password visibility
|
||||
function togglePassword(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const wrapper = input.parentElement;
|
||||
const toggleBtn = wrapper.querySelector('.toggle-password');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
// Change to eye-slash icon
|
||||
toggleBtn.innerHTML = `
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
</svg>
|
||||
`;
|
||||
} else {
|
||||
input.type = 'password';
|
||||
// Change back to eye icon
|
||||
toggleBtn.innerHTML = `
|
||||
<svg class="eye-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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,17 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('Dashboard') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900">
|
||||
{{ __("You're logged in!") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<!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', 'Laravel') }}</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">
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
@include('layouts.navigation')
|
||||
|
||||
<!-- Page Heading -->
|
||||
@isset($header)
|
||||
<header class="bg-white shadow">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{{ $header }}
|
||||
</div>
|
||||
</header>
|
||||
@endisset
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<!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', 'Laravel') }}</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 text-gray-900 antialiased">
|
||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
|
||||
<div>
|
||||
<a href="/">
|
||||
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
|
||||
{{ $slot }}
|
||||
</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()->username }}</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,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,76 @@
|
|||
<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="username" :value="__('Username')" />
|
||||
<x-text-input id="username" name="username" type="text" class="mt-1 block w-full" :value="old('username', $user->username)" required autofocus autocomplete="username" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('username')" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="nama" :value="__('Nama')" />
|
||||
<x-text-input id="nama" name="nama" type="text" class="mt-1 block w-full" :value="old('nama', $user->nama)" required autocomplete="nama" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('nama')" />
|
||||
</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>
|
||||
<x-input-label for="no_hp" :value="__('Nomor HP')" />
|
||||
<x-text-input id="no_hp" name="no_hp" type="text" class="mt-1 block w-full" :value="old('no_hp', $user->no_hp)" required autocomplete="tel" placeholder="08xxxxxxxxxx" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('no_hp')" />
|
||||
</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>
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -1,7 +1,20 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
|
||||
Route::get('/dashboard', function () {
|
||||
return view('dashboard');
|
||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||
});
|
||||
|
||||
require __DIR__.'/auth.php';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
import forms from '@tailwindcss/forms';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||
'./storage/framework/views/*.php',
|
||||
'./resources/views/**/*.blade.php',
|
||||
],
|
||||
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [forms],
|
||||
};
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthenticationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_login_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/login');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_users_can_authenticate_using_the_login_screen(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
public function test_users_can_not_authenticate_with_invalid_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_users_can_logout(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/logout');
|
||||
|
||||
$this->assertGuest();
|
||||
$response->assertRedirect('/');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmailVerificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_email_verification_screen_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/verify-email');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_email_can_be_verified(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
||||
);
|
||||
|
||||
$response = $this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
||||
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
public function test_email_is_not_verified_with_invalid_hash(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1('wrong-email')]
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordConfirmationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_confirm_password_screen_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/confirm-password');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_password_can_be_confirmed(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/confirm-password', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHasNoErrors();
|
||||
}
|
||||
|
||||
public function test_password_is_not_confirmed_with_invalid_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/confirm-password', [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordResetTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_reset_password_link_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/forgot-password');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_reset_password_link_can_be_requested(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/forgot-password', ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
}
|
||||
|
||||
public function test_reset_password_screen_can_be_rendered(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/forgot-password', ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||
$response = $this->get('/reset-password/'.$notification->token);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_password_can_be_reset_with_valid_token(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/forgot-password', ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||
$response = $this->post('/reset-password', [
|
||||
'token' => $notification->token,
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordUpdateTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_password_can_be_updated(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from('/profile')
|
||||
->put('/password', [
|
||||
'current_password' => 'password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_update_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from('/profile')
|
||||
->put('/password', [
|
||||
'current_password' => 'wrong-password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasErrorsIn('updatePassword', 'current_password')
|
||||
->assertRedirect('/profile');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_registration_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/register');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_new_users_can_register(): void
|
||||
{
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProfileTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_profile_page_is_displayed(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get('/profile');
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_profile_information_can_be_updated(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch('/profile', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$user->refresh();
|
||||
|
||||
$this->assertSame('Test User', $user->name);
|
||||
$this->assertSame('test@example.com', $user->email);
|
||||
$this->assertNull($user->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch('/profile', [
|
||||
'name' => 'Test User',
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertNotNull($user->refresh()->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_user_can_delete_their_account(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->delete('/profile', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertGuest();
|
||||
$this->assertNull($user->fresh());
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_delete_account(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from('/profile')
|
||||
->delete('/profile', [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasErrorsIn('userDeletion', 'password')
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertNotNull($user->fresh());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
|
@ -8,11 +7,5 @@ export default defineConfig({
|
|||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue