profile 3 user

This commit is contained in:
RetasyaSalsabila 2026-03-14 18:24:28 +07:00
parent 3e4c585230
commit 7f81522b85
18 changed files with 1189 additions and 580 deletions

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use App\Models\Admin;
class ProfileController extends Controller
{
public function edit()
{
$admin = Auth::guard('admin')->user();
return view('admin.profile.edit', compact('admin'));
}
public function update(Request $request)
{
$admin = Auth::guard('admin')->user();
$request->validate([
'username' => 'required|string|max:100|unique:admins,username,' . $admin->id_admin . ',id_admin',
'password' => 'nullable|min:6|confirmed',
'password_confirmation' => 'nullable',
'foto_profil' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',
], [
'username.required' => 'Username wajib diisi.',
'username.unique' => 'Username sudah digunakan.',
'password.min' => 'Password minimal 6 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
'foto_profil.image' => 'File harus berupa gambar.',
'foto_profil.max' => 'Ukuran foto maksimal 2MB.',
]);
$data = ['username' => $request->username];
if ($request->filled('password')) {
$data['password'] = Hash::make($request->password);
}
if ($request->hasFile('foto_profil')) {
if ($admin->foto_profil && Storage::disk('public')->exists($admin->foto_profil)) {
Storage::disk('public')->delete($admin->foto_profil);
}
$path = $request->file('foto_profil')->store('foto_profil/admin', 'public');
$data['foto_profil'] = $path;
}
$admin->update($data);
return redirect()->route('admin.profile.edit')
->with('success', 'Profil berhasil diperbarui!');
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Http\Controllers\Guru;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class ProfilController extends Controller
{
public function show()
{
$guru = Auth::guard('guru')->user();
return view('guru.profil.show', compact('guru'));
}
public function update(Request $request)
{
$guru = Auth::guard('guru')->user();
$validated = $request->validate([
'nama' => 'required|string|max:100',
'password' => 'nullable|string|min:6|confirmed',
], [
'nama.required' => 'Nama wajib diisi',
'password.min' => 'Password minimal 6 karakter',
'password.confirmed' => 'Konfirmasi password tidak cocok',
]);
$guru->nama = $validated['nama'];
if ($request->filled('password')) {
$guru->password = Hash::make($validated['password']);
}
$guru->save();
return redirect()->route('guru.profil.show')
->with('success', 'Profil berhasil diupdate!');
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Guru;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
class ProfileController extends Controller
{
public function edit()
{
$guru = Auth::guard('guru')->user();
return view('guru.profile.edit', compact('guru'));
}
public function update(Request $request)
{
$guru = Auth::guard('guru')->user();
$request->validate([
'password' => 'nullable|min:6|confirmed',
'password_confirmation' => 'nullable',
'foto_profil' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',
], [
'password.min' => 'Password minimal 6 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
'foto_profil.image' => 'File harus berupa gambar.',
'foto_profil.max' => 'Ukuran foto maksimal 2MB.',
]);
$data = [];
if ($request->filled('password')) {
$data['password'] = Hash::make($request->password);
}
if ($request->hasFile('foto_profil')) {
if ($guru->foto_profil && Storage::disk('public')->exists($guru->foto_profil)) {
Storage::disk('public')->delete($guru->foto_profil);
}
$path = $request->file('foto_profil')->store('foto_profil/guru', 'public');
$data['foto_profil'] = $path;
}
if (!empty($data)) {
$guru->update($data);
}
return redirect()->route('guru.profile.edit')
->with('success', 'Profil berhasil diperbarui!');
}
}

View File

@ -1,60 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Siswa;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
class ProfileController extends Controller
{
public function edit()
{
$siswa = Auth::guard('siswa')->user();
return view('siswa.profile.edit', compact('siswa'));
}
public function update(Request $request)
{
$siswa = Auth::guard('siswa')->user();
$request->validate([
'password' => 'nullable|min:6|confirmed',
'password_confirmation' => 'nullable',
'foto_profil' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',
], [
'password.min' => 'Password minimal 6 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
'foto_profil.image' => 'File harus berupa gambar.',
'foto_profil.max' => 'Ukuran foto maksimal 2MB.',
]);
$data = [];
// Update password
if ($request->filled('password')) {
$data['password'] = Hash::make($request->password);
}
// Update foto profil
if ($request->hasFile('foto_profil')) {
// Hapus foto lama jika ada
if ($siswa->foto_profil && Storage::disk('public')->exists($siswa->foto_profil)) {
Storage::disk('public')->delete($siswa->foto_profil);
}
$path = $request->file('foto_profil')->store('foto_profil/siswa', 'public');
$data['foto_profil'] = $path;
}
if (!empty($data)) {
$siswa->update($data);
}
return redirect()->route('siswa.profile.edit')
->with('success', 'Profil berhasil diperbarui!');
}
}

View File

@ -19,7 +19,8 @@ class Admin extends Authenticatable
protected $fillable = [ protected $fillable = [
'username', 'username',
'password' 'password',
'foto_profil'
]; ];
protected $hidden = [ protected $hidden = [

View File

@ -17,7 +17,8 @@ class Guru extends Authenticatable
protected $fillable = [ protected $fillable = [
'nip', 'nip',
'nama', 'nama',
'password' 'password',
'foto_profil'
]; ];
protected $hidden = [ protected $hidden = [

View File

@ -21,6 +21,7 @@ class Siswa extends Authenticatable
'tanggal_lahir', 'tanggal_lahir',
'id_kelas', 'id_kelas',
'password', 'password',
'foto_profil'
]; ];
protected $hidden = [ protected $hidden = [

View File

@ -0,0 +1,42 @@
<?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
{
if (!Schema::hasColumn('siswas', 'foto_profil')) {
Schema::table('siswas', function (Blueprint $table) {
$table->string('foto_profil')->nullable()->after('password');
});
}
if (!Schema::hasColumn('gurus', 'foto_profil')) {
Schema::table('gurus', function (Blueprint $table) {
$table->string('foto_profil')->nullable()->after('password');
});
}
if (!Schema::hasColumn('admins', 'foto_profil')) {
Schema::table('admins', function (Blueprint $table) {
$table->string('foto_profil')->nullable()->after('password');
});
}
}
public function down(): void
{
Schema::table('siswas', function (Blueprint $table) {
$table->dropColumn('foto_profil');
});
Schema::table('gurus', function (Blueprint $table) {
$table->dropColumn('foto_profil');
});
Schema::table('admins', function (Blueprint $table) {
$table->dropColumn('foto_profil');
});
}
};

View File

@ -9,92 +9,42 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style> <style>
body { body { font-family: 'Poppins', sans-serif; background-color: #f8f9fa; margin: 0; }
font-family: 'Poppins', sans-serif;
background-color: #f8f9fa;
margin: 0;
}
.admin-wrapper { display: flex; min-height: 100vh; } .admin-wrapper { display: flex; min-height: 100vh; }
.sidebar { .sidebar { width: 260px; background: #ffffff; border-right: 2px solid #e6f0ff; padding: 30px 20px; display: flex; flex-direction: column; }
width: 260px;
background: #ffffff;
border-right: 2px solid #e6f0ff;
padding: 30px 20px;
display: flex;
flex-direction: column;
}
.sidebar-logo { text-align: center; margin-bottom: 40px; } .sidebar-logo { text-align: center; margin-bottom: 40px; }
.sidebar-logo img { width: 90px; } .sidebar-logo img { width: 90px; }
.sidebar-menu { display: flex; flex-direction: column; } .sidebar-menu { display: flex; flex-direction: column; }
.sidebar-link { .sidebar-link { display: flex; align-items: center; gap: 12px; padding: 12px 18px; margin-bottom: 12px; border-radius: 12px; color: #64748b; text-decoration: none; font-weight: 500; transition: all 0.2s ease; }
display: flex;
align-items: center;
gap: 12px;
padding: 12px 18px;
margin-bottom: 12px;
border-radius: 12px;
color: #64748b;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
}
.sidebar-link:hover { background: #e6f0ff; color: #1d4ed8; } .sidebar-link:hover { background: #e6f0ff; color: #1d4ed8; }
.sidebar-link.active { background: #e6f0ff; color: #1d4ed8; } .sidebar-link.active { background: #e6f0ff; color: #1d4ed8; }
.sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; } .sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; }
.sidebar-section { .sidebar-section { font-size: 10px; font-weight: 700; color: #94a3b8; letter-spacing: 1px; text-transform: uppercase; padding: 4px 18px; margin-bottom: 6px; margin-top: 6px; }
font-size: 10px;
font-weight: 700;
color: #94a3b8;
letter-spacing: 1px;
text-transform: uppercase;
padding: 4px 18px;
margin-bottom: 6px;
margin-top: 6px;
}
.sidebar-logout { margin-top: auto; } .sidebar-logout { margin-top: auto; }
.sidebar-logout button { width: 100%; border: none; background: transparent; color: #ef4444; font-weight: 600; padding: 10px; text-align: left; }
.sidebar-logout button {
width: 100%;
border: none;
background: transparent;
color: #ef4444;
font-weight: 600;
padding: 10px;
text-align: left;
}
.main { flex: 1; background: #f5f9ff; display: flex; flex-direction: column; } .main { flex: 1; background: #f5f9ff; display: flex; flex-direction: column; }
.topbar { .topbar { background: #2b8ef3; margin: 20px; padding: 16px 24px; border-radius: 16px; color: white; display: flex; justify-content: space-between; align-items: center; }
background: #2b8ef3;
margin: 20px;
padding: 16px 24px;
border-radius: 16px;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.topbar-left { font-weight: 600; font-size: 16px; } .topbar-left { font-weight: 600; font-size: 16px; }
.topbar-right { display: flex; align-items: center; gap: 16px; } .topbar-right { display: flex; align-items: center; gap: 14px; }
.topbar-icon { width: 24px; height: 24px; cursor: pointer; } .topbar-icon { width: 24px; height: 24px; cursor: pointer; }
.topbar-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid rgba(255,255,255,0.5); cursor: pointer; transition: border-color 0.2s; display: block; }
.topbar-avatar:hover { border-color: white; }
.topbar-avatar-icon { width: 36px; height: 36px; border-radius: 50%; background: rgba(255,255,255,0.2); border: 2px solid rgba(255,255,255,0.4); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.2s; flex-shrink: 0; text-decoration: none; }
.topbar-avatar-icon:hover { background: rgba(255,255,255,0.3); }
.topbar-avatar-icon svg { width: 20px; height: 20px; stroke: white; }
.content { padding: 20px 30px; flex: 1; } .content { padding: 20px 30px; flex: 1; }
</style> </style>
@stack('styles') @stack('styles')
</head> </head>
<body> <body>
<div class="admin-wrapper"> <div class="admin-wrapper">
@ -104,67 +54,39 @@
</div> </div>
<nav class="sidebar-menu"> <nav class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<a href="{{ route('admin.dashboard') }}" <img src="{{ asset('images/icon/sidebar/home.png') }}" class="sidebar-icon" alt=""><span>Dashboard</span>
class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/home.png') }}" class="sidebar-icon" alt="">
<span>Dashboard</span>
</a> </a>
<div class="sidebar-section">Data Master</div> <div class="sidebar-section">Data Master</div>
<a href="{{ route('admin.guru.index') }}" class="sidebar-link {{ request()->routeIs('admin.guru.*') ? 'active' : '' }}">
<a href="{{ route('admin.guru.index') }}" <img src="{{ asset('images/icon/sidebar/guru.png') }}" class="sidebar-icon" alt=""><span>Daftar Guru</span>
class="sidebar-link {{ request()->routeIs('admin.guru.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/guru.png') }}" class="sidebar-icon" alt="">
<span>Daftar Guru</span>
</a> </a>
<a href="{{ route('admin.kelas.index') }}" class="sidebar-link {{ request()->routeIs('admin.kelas.*') ? 'active' : '' }}">
<a href="{{ route('admin.kelas.index') }}" <img src="{{ asset('images/icon/sidebar/kelas.png') }}" class="sidebar-icon" alt=""><span>Daftar Kelas</span>
class="sidebar-link {{ request()->routeIs('admin.kelas.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/kelas.png') }}" class="sidebar-icon" alt="">
<span>Daftar Kelas</span>
</a> </a>
<a href="{{ route('admin.siswa.index') }}" class="sidebar-link {{ request()->routeIs('admin.siswa.*') ? 'active' : '' }}">
<a href="{{ route('admin.siswa.index') }}" <img src="{{ asset('images/icon/sidebar/siswa.png') }}" class="sidebar-icon" alt=""><span>Daftar Siswa</span>
class="sidebar-link {{ request()->routeIs('admin.siswa.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/siswa.png') }}" class="sidebar-icon" alt="">
<span>Daftar Siswa</span>
</a> </a>
<a href="{{ route('admin.mapel.index') }}" class="sidebar-link {{ request()->routeIs('admin.mapel.*') ? 'active' : '' }}">
<a href="{{ route('admin.mapel.index') }}" <img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon" alt=""><span>Mata Pelajaran</span>
class="sidebar-link {{ request()->routeIs('admin.mapel.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon" alt="">
<span>Mata Pelajaran</span>
</a> </a>
<div class="sidebar-section">Konten Guru</div> <div class="sidebar-section">Konten Guru</div>
<a href="{{ route('admin.materi.history') }}" class="sidebar-link {{ request()->routeIs('admin.materi.*') ? 'active' : '' }}">
<a href="{{ route('admin.materi.history') }}" <img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon" alt=""><span>History Materi</span>
class="sidebar-link {{ request()->routeIs('admin.materi.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon" alt="">
<span>History Materi</span>
</a> </a>
<a href="{{ route('admin.tugas.history') }}" class="sidebar-link {{ request()->routeIs('admin.tugas.*') ? 'active' : '' }}">
<a href="{{ route('admin.tugas.history') }}" <img src="{{ asset('images/icon/sidebar/guru.png') }}" class="sidebar-icon" alt=""><span>History Tugas</span>
class="sidebar-link {{ request()->routeIs('admin.tugas.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/guru.png') }}" class="sidebar-icon" alt="">
<span>History Tugas</span>
</a> </a>
<div class="sidebar-section">Gamifikasi</div> <div class="sidebar-section">Gamifikasi</div>
<a href="{{ route('admin.challenge.index') }}" class="sidebar-link {{ request()->routeIs('admin.challenge.*') ? 'active' : '' }}">
<a href="{{ route('admin.challenge.index') }}" <img src="{{ asset('images/icon/sidebar/challenge.png') }}" class="sidebar-icon" alt=""><span>Challenge</span>
class="sidebar-link {{ request()->routeIs('admin.challenge.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/challenge.png') }}" class="sidebar-icon" alt="">
<span>Challenge</span>
</a> </a>
<a href="{{ route('admin.leaderboard.index') }}" class="sidebar-link {{ request()->routeIs('admin.leaderboard.*') ? 'active' : '' }}">
<a href="{{ route('admin.leaderboard.index') }}" <img src="{{ asset('images/icon/sidebar/lb.png') }}" class="sidebar-icon" alt=""><span>Leaderboard</span>
class="sidebar-link {{ request()->routeIs('admin.leaderboard.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/lb.png') }}" class="sidebar-icon" alt="">
<span>Leaderboard</span>
</a> </a>
</nav> </nav>
<form action="{{ route('admin.logout') }}" method="POST" class="sidebar-logout"> <form action="{{ route('admin.logout') }}" method="POST" class="sidebar-logout">
@ -180,7 +102,19 @@ class="sidebar-link {{ request()->routeIs('admin.leaderboard.*') ? 'active' : ''
</div> </div>
<div class="topbar-right"> <div class="topbar-right">
<img src="{{ asset('images/icon/sidebar/notif.png') }}" class="topbar-icon" alt="Notification"> <img src="{{ asset('images/icon/sidebar/notif.png') }}" class="topbar-icon" alt="Notification">
<img src="{{ asset('images/icon/sidebar/profil.png') }}" class="topbar-icon" alt="Profile">
@php $admin = Auth::guard('admin')->user(); @endphp
@if($admin->foto_profil)
<a href="{{ route('admin.profile.edit') }}">
<img src="{{ Storage::url($admin->foto_profil) }}" class="topbar-avatar" alt="Profil">
</a>
@else
<a href="{{ route('admin.profile.edit') }}" class="topbar-avatar-icon">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
</svg>
</a>
@endif
</div> </div>
</header> </header>

View File

@ -0,0 +1,233 @@
@extends('admin.layouts.app')
@section('title', 'Edit Profil')
@push('styles')
<style>
.page-title { font-size: 24px; font-weight: 800; margin-bottom: 6px; }
.page-subtitle { font-size: 14px; color: #64748b; margin-bottom: 28px; }
.profile-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 32px;
max-width: 560px;
}
.foto-wrap {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 32px;
padding-bottom: 28px;
border-bottom: 1px solid #f1f5f9;
}
.foto-preview {
width: 88px; height: 88px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #e6f0ff;
flex-shrink: 0;
}
.foto-placeholder {
width: 88px; height: 88px;
border-radius: 50%;
background: #e6f0ff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 3px solid #e6f0ff;
}
.foto-placeholder svg { width: 40px; height: 40px; color: #2b8ef3; }
.foto-info p { font-size: 13px; color: #64748b; margin: 0 0 10px; }
.btn-upload {
display: inline-block;
background: #e6f0ff;
color: #2b8ef3;
font-size: 13px;
font-weight: 700;
padding: 8px 18px;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s;
}
.btn-upload:hover { background: #dbeeff; }
.field-group { margin-bottom: 18px; }
.field-label {
display: block;
font-size: 12px;
font-weight: 700;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.field-label span {
color: #94a3b8;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
margin-left: 4px;
}
.field-input {
width: 100%;
background: #f8fafc;
border: 1.5px solid #e2e8f0;
border-radius: 12px;
padding: 11px 14px;
font-size: 14px;
font-family: 'Poppins', sans-serif;
color: #1e293b;
outline: none;
transition: all 0.2s;
}
.field-input:focus {
border-color: #2b8ef3;
background: white;
box-shadow: 0 0 0 3px rgba(43,142,243,0.1);
}
.divider { border: none; border-top: 1px solid #f1f5f9; margin: 24px 0; }
.btn-save {
background: #2b8ef3;
color: white;
border: none;
border-radius: 12px;
padding: 12px 28px;
font-size: 14px;
font-weight: 700;
font-family: 'Poppins', sans-serif;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 14px rgba(43,142,243,0.3);
}
.btn-save:hover { background: #1a7ae0; transform: translateY(-1px); }
.alert-success {
background: #f0fdf4;
border: 1.5px solid #86efac;
color: #166534;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
margin-bottom: 20px;
}
.alert-error {
background: #fef2f2;
border: 1.5px solid #fca5a5;
color: #991b1b;
border-radius: 12px;
padding: 12px 16px;
font-size: 13px;
margin-bottom: 20px;
}
</style>
@endpush
@section('content')
@php $admin = Auth::guard('admin')->user(); @endphp
<h3 class="page-title">Edit Profil</h3>
<p class="page-subtitle">Perbarui username, foto profil, dan password akunmu.</p>
@if(session('success'))
<div class="alert-success"> {{ session('success') }}</div>
@endif
@if($errors->any())
<div class="alert-error">
<ul style="margin:0;padding-left:16px">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.profile.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="profile-card">
<div class="foto-wrap">
@if($admin->foto_profil)
<img src="{{ Storage::url($admin->foto_profil) }}" class="foto-preview" id="foto-preview" alt="Foto Profil">
@else
<div class="foto-placeholder" id="foto-placeholder">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
</svg>
</div>
<img src="" class="foto-preview" id="foto-preview" alt="" style="display:none">
@endif
<div class="foto-info">
<p>Format: JPG, PNG, WEBP. Maks. 2MB.</p>
<label for="foto_profil" class="btn-upload">Pilih Foto</label>
<input type="file" name="foto_profil" id="foto_profil" accept="image/*" style="display:none">
</div>
</div>
{{-- Username bisa diubah untuk admin --}}
<div class="field-group">
<label class="field-label">Username</label>
<input type="text" name="username" class="field-input"
value="{{ old('username', $admin->username) }}" required>
</div>
<hr class="divider">
<div class="field-group">
<label class="field-label">Password Baru <span>(kosongkan jika tidak ingin mengubah)</span></label>
<input type="password" name="password" class="field-input"
placeholder="Masukkan password baru" autocomplete="new-password">
</div>
<div class="field-group">
<label class="field-label">Konfirmasi Password Baru</label>
<input type="password" name="password_confirmation" class="field-input"
placeholder="Ulangi password baru" autocomplete="new-password">
</div>
<div style="margin-top:8px">
<button type="submit" class="btn-save">Simpan Perubahan</button>
</div>
</div>
</form>
@endsection
@push('scripts')
<script>
document.getElementById('foto_profil').addEventListener('change', function () {
const file = this.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
const preview = document.getElementById('foto-preview');
const placeholder = document.getElementById('foto-placeholder');
preview.src = url;
preview.style.display = 'block';
if (placeholder) placeholder.style.display = 'none';
});
</script>
@endpush

View File

@ -9,77 +9,39 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style> <style>
body { body { font-family: 'Poppins', sans-serif; background-color: #f8f9fa; margin: 0; }
font-family: 'Poppins', sans-serif;
background-color: #f8f9fa;
margin: 0;
}
.wrapper { display: flex; min-height: 100vh; } .wrapper { display: flex; min-height: 100vh; }
.sidebar { .sidebar { width: 260px; background: #ffffff; border-right: 2px solid #e6f0ff; padding: 30px 20px; display: flex; flex-direction: column; }
width: 260px;
background: #ffffff;
border-right: 2px solid #e6f0ff;
padding: 30px 20px;
display: flex;
flex-direction: column;
}
.sidebar-logo { text-align: center; margin-bottom: 40px; } .sidebar-logo { text-align: center; margin-bottom: 40px; }
.sidebar-logo img { width: 90px; } .sidebar-logo img { width: 90px; }
.sidebar-link { .sidebar-link { display: flex; align-items: center; gap: 12px; padding: 12px 18px; margin-bottom: 12px; border-radius: 12px; color: #64748b; text-decoration: none; font-weight: 500; transition: all 0.2s ease; }
display: flex;
align-items: center;
gap: 12px;
padding: 12px 18px;
margin-bottom: 12px;
border-radius: 12px;
color: #64748b;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
}
.sidebar-link:hover { background: #e6f0ff; color: #1d4ed8; } .sidebar-link:hover { background: #e6f0ff; color: #1d4ed8; }
.sidebar-link.active { background: #e6f0ff; color: #1d4ed8; } .sidebar-link.active { background: #e6f0ff; color: #1d4ed8; }
.sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; } .sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; }
.sidebar-logout { margin-top: auto; } .sidebar-logout { margin-top: auto; }
.main { flex: 1; background: #f5f9ff; display: flex; flex-direction: column; } .main { flex: 1; background: #f5f9ff; display: flex; flex-direction: column; }
.topbar { .topbar { background: #2b8ef3; margin: 20px; padding: 16px 24px; border-radius: 16px; color: white; display: flex; justify-content: space-between; align-items: center; }
background: #2b8ef3; .topbar-left { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 16px; }
margin: 20px;
padding: 16px 24px;
border-radius: 16px;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.topbar-left {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 16px;
}
.topbar-waving { width: 26px; height: 26px; } .topbar-waving { width: 26px; height: 26px; }
.topbar-right { display: flex; align-items: center; gap: 14px; }
.topbar-right { display: flex; align-items: center; gap: 16px; }
.topbar-icon { width: 24px; height: 24px; cursor: pointer; } .topbar-icon { width: 24px; height: 24px; cursor: pointer; }
.topbar-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid rgba(255,255,255,0.5); cursor: pointer; transition: border-color 0.2s; display: block; }
.topbar-avatar:hover { border-color: white; }
.topbar-avatar-icon { width: 36px; height: 36px; border-radius: 50%; background: rgba(255,255,255,0.2); border: 2px solid rgba(255,255,255,0.4); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.2s; flex-shrink: 0; text-decoration: none; }
.topbar-avatar-icon:hover { background: rgba(255,255,255,0.3); }
.topbar-avatar-icon svg { width: 20px; height: 20px; stroke: white; }
.content { padding: 20px 30px; flex: 1; } .content { padding: 20px 30px; flex: 1; }
</style> </style>
@stack('styles') @stack('styles')
</head> </head>
<body> <body>
<div class="wrapper"> <div class="wrapper">
@ -88,40 +50,23 @@
<img src="{{ asset('images/logo/logosmk.png') }}" alt="Logo"> <img src="{{ asset('images/logo/logosmk.png') }}" alt="Logo">
</div> </div>
<a href="{{ route('guru.dashboard') }}" <a href="{{ route('guru.dashboard') }}" class="sidebar-link {{ request()->routeIs('guru.dashboard') ? 'active' : '' }}">
class="sidebar-link {{ request()->routeIs('guru.dashboard') ? 'active' : '' }}"> <img src="{{ asset('images/icon/sidebar/home.png') }}" class="sidebar-icon"><span>Dashboard</span>
<img src="{{ asset('images/icon/sidebar/home.png') }}" class="sidebar-icon">
<span>Dashboard</span>
</a> </a>
<a href="{{ route('guru.guru.index') }}" class="sidebar-link {{ request()->routeIs('guru.guru.*') ? 'active' : '' }}">
<a href="{{ route('guru.guru.index') }}" <img src="{{ asset('images/icon/sidebar/guru.png') }}" class="sidebar-icon"><span>Daftar Guru</span>
class="sidebar-link {{ request()->routeIs('guru.guru.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/guru.png') }}" class="sidebar-icon">
<span>Daftar Guru</span>
</a> </a>
<a href="{{ route('guru.kelas.index') }}" class="sidebar-link {{ request()->routeIs('guru.kelas.*') ? 'active' : '' }}">
<a href="{{ route('guru.kelas.index') }}" <img src="{{ asset('images/icon/sidebar/kelas.png') }}" class="sidebar-icon"><span>Daftar Kelas</span>
class="sidebar-link {{ request()->routeIs('guru.kelas.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/kelas.png') }}" class="sidebar-icon">
<span>Daftar Kelas</span>
</a> </a>
<a href="{{ route('guru.siswa.index') }}" class="sidebar-link {{ request()->routeIs('guru.siswa.*') ? 'active' : '' }}">
<a href="{{ route('guru.siswa.index') }}" <img src="{{ asset('images/icon/sidebar/siswa.png') }}" class="sidebar-icon"><span>Daftar Siswa</span>
class="sidebar-link {{ request()->routeIs('guru.siswa.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/siswa.png') }}" class="sidebar-icon">
<span>Daftar Siswa</span>
</a> </a>
<a href="{{ route('guru.mapel.index') }}" class="sidebar-link {{ request()->routeIs('guru.mapel.*') ? 'active' : '' }}">
<a href="{{ route('guru.mapel.index') }}" <img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon"><span>Mata Pelajaran</span>
class="sidebar-link {{ request()->routeIs('guru.mapel.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon">
<span>Mata Pelajaran</span>
</a> </a>
<a href="{{ route('guru.leaderboard.index') }}" class="sidebar-link {{ request()->routeIs('guru.leaderboard.*') ? 'active' : '' }}">
<a href="{{ route('guru.leaderboard.index') }}" <img src="{{ asset('images/icon/sidebar/lb.png') }}" class="sidebar-icon"><span>Leaderboard</span>
class="sidebar-link {{ request()->routeIs('guru.leaderboard.*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/lb.png') }}" class="sidebar-icon">
<span>Leaderboard</span>
</a> </a>
<form action="{{ route('guru.logout') }}" method="POST" class="sidebar-logout"> <form action="{{ route('guru.logout') }}" method="POST" class="sidebar-logout">
@ -138,7 +83,19 @@ class="sidebar-link {{ request()->routeIs('guru.leaderboard.*') ? 'active' : ''
</div> </div>
<div class="topbar-right"> <div class="topbar-right">
<img src="{{ asset('images/icon/sidebar/notif.png') }}" class="topbar-icon" alt="Notification"> <img src="{{ asset('images/icon/sidebar/notif.png') }}" class="topbar-icon" alt="Notification">
<img src="{{ asset('images/icon/sidebar/profil.png') }}" class="topbar-icon" alt="Profile">
@php $guru = Auth::guard('guru')->user(); @endphp
@if($guru->foto_profil)
<a href="{{ route('guru.profile.edit') }}">
<img src="{{ Storage::url($guru->foto_profil) }}" class="topbar-avatar" alt="Profil">
</a>
@else
<a href="{{ route('guru.profile.edit') }}" class="topbar-avatar-icon">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
</svg>
</a>
@endif
</div> </div>
</header> </header>
@ -148,7 +105,6 @@ class="sidebar-link {{ request()->routeIs('guru.leaderboard.*') ? 'active' : ''
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
@stack('scripts') @stack('scripts')
</body> </body>

View File

@ -0,0 +1,243 @@
@extends('guru.layouts.app')
@section('title', 'Edit Profil')
@push('styles')
<style>
.page-title { font-size: 24px; font-weight: 800; margin-bottom: 6px; }
.page-subtitle { font-size: 14px; color: #64748b; margin-bottom: 28px; }
.profile-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 32px;
max-width: 560px;
}
.foto-wrap {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 32px;
padding-bottom: 28px;
border-bottom: 1px solid #f1f5f9;
}
.foto-preview {
width: 88px; height: 88px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #e6f0ff;
flex-shrink: 0;
}
.foto-placeholder {
width: 88px; height: 88px;
border-radius: 50%;
background: #e6f0ff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 3px solid #e6f0ff;
}
.foto-placeholder svg { width: 40px; height: 40px; color: #2b8ef3; }
.foto-info p { font-size: 13px; color: #64748b; margin: 0 0 10px; }
.btn-upload {
display: inline-block;
background: #e6f0ff;
color: #2b8ef3;
font-size: 13px;
font-weight: 700;
padding: 8px 18px;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s;
border: none;
}
.btn-upload:hover { background: #dbeeff; }
.field-group { margin-bottom: 18px; }
.field-label {
display: block;
font-size: 12px;
font-weight: 700;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.field-label span {
color: #94a3b8;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
margin-left: 4px;
}
.field-input {
width: 100%;
background: #f8fafc;
border: 1.5px solid #e2e8f0;
border-radius: 12px;
padding: 11px 14px;
font-size: 14px;
font-family: 'Poppins', sans-serif;
color: #1e293b;
outline: none;
transition: all 0.2s;
}
.field-input:focus {
border-color: #2b8ef3;
background: white;
box-shadow: 0 0 0 3px rgba(43,142,243,0.1);
}
.field-input:disabled {
background: #f1f5f9;
color: #94a3b8;
cursor: not-allowed;
}
.divider { border: none; border-top: 1px solid #f1f5f9; margin: 24px 0; }
.btn-save {
background: #2b8ef3;
color: white;
border: none;
border-radius: 12px;
padding: 12px 28px;
font-size: 14px;
font-weight: 700;
font-family: 'Poppins', sans-serif;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 14px rgba(43,142,243,0.3);
}
.btn-save:hover { background: #1a7ae0; transform: translateY(-1px); }
.alert-success {
background: #f0fdf4;
border: 1.5px solid #86efac;
color: #166534;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
margin-bottom: 20px;
}
.alert-error {
background: #fef2f2;
border: 1.5px solid #fca5a5;
color: #991b1b;
border-radius: 12px;
padding: 12px 16px;
font-size: 13px;
margin-bottom: 20px;
}
</style>
@endpush
@section('content')
@php $guru = Auth::guard('guru')->user(); @endphp
<h3 class="page-title">Edit Profil</h3>
<p class="page-subtitle">Perbarui foto profil dan password akunmu.</p>
@if(session('success'))
<div class="alert-success"> {{ session('success') }}</div>
@endif
@if($errors->any())
<div class="alert-error">
<ul style="margin:0;padding-left:16px">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('guru.profile.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="profile-card">
<div class="foto-wrap">
@if($guru->foto_profil)
<img src="{{ Storage::url($guru->foto_profil) }}" class="foto-preview" id="foto-preview" alt="Foto Profil">
@else
<div class="foto-placeholder" id="foto-placeholder">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
</svg>
</div>
<img src="" class="foto-preview" id="foto-preview" alt="" style="display:none">
@endif
<div class="foto-info">
<p>Format: JPG, PNG, WEBP. Maks. 2MB.</p>
<label for="foto_profil" class="btn-upload">Pilih Foto</label>
<input type="file" name="foto_profil" id="foto_profil" accept="image/*" style="display:none">
</div>
</div>
<div class="field-group">
<label class="field-label">NIP <span>(tidak dapat diubah)</span></label>
<input type="text" class="field-input" value="{{ $guru->nip }}" disabled>
</div>
<div class="field-group">
<label class="field-label">Nama Lengkap <span>(tidak dapat diubah)</span></label>
<input type="text" class="field-input" value="{{ $guru->nama }}" disabled>
</div>
<hr class="divider">
<div class="field-group">
<label class="field-label">Password Baru <span>(kosongkan jika tidak ingin mengubah)</span></label>
<input type="password" name="password" class="field-input"
placeholder="Masukkan password baru" autocomplete="new-password">
</div>
<div class="field-group">
<label class="field-label">Konfirmasi Password Baru</label>
<input type="password" name="password_confirmation" class="field-input"
placeholder="Ulangi password baru" autocomplete="new-password">
</div>
<div style="margin-top:8px">
<button type="submit" class="btn-save">Simpan Perubahan</button>
</div>
</div>
</form>
@endsection
@push('scripts')
<script>
document.getElementById('foto_profil').addEventListener('change', function () {
const file = this.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
const preview = document.getElementById('foto-preview');
const placeholder = document.getElementById('foto-placeholder');
preview.src = url;
preview.style.display = 'block';
if (placeholder) placeholder.style.display = 'none';
});
</script>
@endpush

View File

@ -35,28 +35,16 @@
position: relative; position: relative;
} }
/* Default: collapsed saat pertama load */
.sidebar.collapsed { .sidebar.collapsed {
width: 0; width: 0;
padding: 0; padding: 0;
border-right: none; border-right: none;
} }
.sidebar-logo { .sidebar-logo { text-align: center; margin-bottom: 40px; white-space: nowrap; }
text-align: center; .sidebar-logo img { width: 90px; }
margin-bottom: 40px;
white-space: nowrap;
}
.sidebar-logo img { .sidebar-menu { display: flex; flex-direction: column; white-space: nowrap; }
width: 90px;
}
.sidebar-menu {
display: flex;
flex-direction: column;
white-space: nowrap;
}
.sidebar-link { .sidebar-link {
display: flex; display: flex;
@ -71,42 +59,19 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.sidebar-link:hover { .sidebar-link:hover { background: #e6f0ff; color: #1d4ed8; }
background: #e6f0ff; .sidebar-link.active { background: #e6f0ff; color: #1d4ed8; }
color: #1d4ed8;
}
.sidebar-link.active { .sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; }
background: #e6f0ff; .sidebar-logout { margin-top: auto; }
color: #1d4ed8; .sidebar-logout button { width: 100%; border: none; background: transparent; color: #ef4444; font-weight: 600; padding: 10px; text-align: left; }
}
.sidebar-icon { /* ===== TOGGLE BUTTON ===== */
width: 20px;
height: 20px;
flex-shrink: 0;
}
.sidebar-logout {
margin-top: auto;
}
.sidebar-logout button {
width: 100%;
border: none;
background: transparent;
color: #ef4444;
font-weight: 600;
padding: 10px;
text-align: left;
}
/* ===== TOGGLE ARROW BUTTON ===== */
.sidebar-toggle-btn { .sidebar-toggle-btn {
position: fixed; position: fixed;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
left: 260px; /* sama dengan lebar sidebar saat terbuka */ left: 260px;
z-index: 1000; z-index: 1000;
width: 22px; width: 22px;
height: 48px; height: 48px;
@ -121,19 +86,11 @@
box-shadow: 2px 0 8px rgba(43, 142, 243, 0.3); box-shadow: 2px 0 8px rgba(43, 142, 243, 0.3);
} }
.sidebar-toggle-btn:hover { .sidebar-toggle-btn:hover { background: #1a7ae0; }
background: #1a7ae0; .sidebar-toggle-btn.collapsed { left: 0; }
}
/* Posisi tombol saat sidebar collapsed */
.sidebar-toggle-btn.collapsed {
left: 0;
}
/* Arrow icon SVG */
.toggle-arrow { .toggle-arrow {
width: 12px; width: 12px; height: 12px;
height: 12px;
fill: none; fill: none;
stroke: white; stroke: white;
stroke-width: 2.5; stroke-width: 2.5;
@ -142,20 +99,10 @@
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
/* Saat collapsed, panah mengarah ke kanan (buka) */ .sidebar-toggle-btn.collapsed .toggle-arrow { transform: rotate(180deg); }
.sidebar-toggle-btn.collapsed .toggle-arrow {
transform: rotate(180deg);
}
/* ===== MAIN ===== */ /* ===== MAIN ===== */
.main { .main { flex: 1; background: #f5f9ff; display: flex; flex-direction: column; transition: all 0.3s ease; min-width: 0; }
flex: 1;
background: #f5f9ff;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
min-width: 0; /* prevent overflow */
}
/* TOPBAR */ /* TOPBAR */
.topbar { .topbar {
@ -169,31 +116,40 @@
align-items: center; align-items: center;
} }
.topbar-left { .topbar-left { font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 10px; }
font-weight: 600; .topbar-right { display: flex; align-items: center; gap: 14px; }
font-size: 16px; .topbar-icon { width: 24px; height: 24px; cursor: pointer; }
display: flex;
align-items: center;
gap: 10px;
}
.topbar-right { /* Foto profil di topbar */
display: flex; .topbar-avatar {
align-items: center; width: 36px; height: 36px;
gap: 16px; border-radius: 50%;
} object-fit: cover;
border: 2px solid rgba(255,255,255,0.5);
.topbar-icon {
width: 24px;
height: 24px;
cursor: pointer; cursor: pointer;
transition: border-color 0.2s;
display: block;
} }
/* CONTENT */ .topbar-avatar:hover { border-color: white; }
.content {
padding: 20px 30px; .topbar-avatar-icon {
flex: 1; width: 36px; height: 36px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
border: 2px solid rgba(255,255,255,0.4);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
flex-shrink: 0;
} }
.topbar-avatar-icon:hover { background: rgba(255,255,255,0.3); }
.topbar-avatar-icon svg { width: 20px; height: 20px; stroke: white; }
.content { padding: 20px 30px; flex: 1; }
</style> </style>
@stack('styles') @stack('styles')
@ -202,38 +158,32 @@
<body> <body>
<div class="siswa-wrapper"> <div class="siswa-wrapper">
<!-- SIDEBAR (collapsed by default) -->
<aside class="sidebar collapsed" id="mainSidebar"> <aside class="sidebar collapsed" id="mainSidebar">
<div class="sidebar-logo"> <div class="sidebar-logo">
<img src="{{ asset('images/logo/logosmk.png') }}" alt="Logo SMK"> <img src="{{ asset('images/logo/logosmk.png') }}" alt="Logo SMK">
</div> </div>
<nav class="sidebar-menu"> <nav class="sidebar-menu">
<a href="{{ route('siswa.dashboard') }}" <a href="{{ route('siswa.dashboard') }}"
class="sidebar-link {{ request()->routeIs('siswa.dashboard') ? 'active' : '' }}"> class="sidebar-link {{ request()->routeIs('siswa.dashboard') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/home.png') }}" class="sidebar-icon" alt=""> <img src="{{ asset('images/icon/sidebar/home.png') }}" class="sidebar-icon" alt="">
<span>Dashboard</span> <span>Dashboard</span>
</a> </a>
<a href="{{ route('siswa.materi.index') }}" <a href="{{ route('siswa.materi.index') }}"
class="sidebar-link {{ request()->routeIs('siswa.materi*') ? 'active' : '' }}"> class="sidebar-link {{ request()->routeIs('siswa.materi*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon" alt=""> <img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon" alt="">
<span>Materi</span> <span>Materi</span>
</a> </a>
<a href="{{ route('siswa.tugas.index') }}" <a href="{{ route('siswa.tugas.index') }}"
class="sidebar-link {{ request()->routeIs('siswa.tugas*') ? 'active' : '' }}"> class="sidebar-link {{ request()->routeIs('siswa.tugas*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/siswa.png') }}" class="sidebar-icon" alt=""> <img src="{{ asset('images/icon/sidebar/siswa.png') }}" class="sidebar-icon" alt="">
<span>Tugas</span> <span>Tugas</span>
</a> </a>
<a href="{{ route('siswa.challenge.index') }}" <a href="{{ route('siswa.challenge.index') }}"
class="sidebar-link {{ request()->routeIs('siswa.challenge*') ? 'active' : '' }}"> class="sidebar-link {{ request()->routeIs('siswa.challenge*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/challenge.png') }}" class="sidebar-icon" alt=""> <img src="{{ asset('images/icon/sidebar/challenge.png') }}" class="sidebar-icon" alt="">
<span>Challenge</span> <span>Challenge</span>
</a> </a>
<a href="{{ route('siswa.leaderboard.index') }}" <a href="{{ route('siswa.leaderboard.index') }}"
class="sidebar-link {{ request()->routeIs('siswa.leaderboard*') ? 'active' : '' }}"> class="sidebar-link {{ request()->routeIs('siswa.leaderboard*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/lb.png') }}" class="sidebar-icon" alt=""> <img src="{{ asset('images/icon/sidebar/lb.png') }}" class="sidebar-icon" alt="">
@ -243,24 +193,18 @@ class="sidebar-link {{ request()->routeIs('siswa.leaderboard*') ? 'active' : ''
<form action="{{ route('siswa.logout') }}" method="POST" class="sidebar-logout"> <form action="{{ route('siswa.logout') }}" method="POST" class="sidebar-logout">
@csrf @csrf
<button type="submit" class="btn btn-danger w-100"> <button type="submit" class="btn btn-danger w-100">Logout</button>
Logout
</button>
</form> </form>
</aside> </aside>
<!-- TOGGLE ARROW BUTTON (collapsed by default karena sidebar tertutup) -->
<button class="sidebar-toggle-btn collapsed" id="sidebarToggleBtn" title="Toggle Sidebar"> <button class="sidebar-toggle-btn collapsed" id="sidebarToggleBtn" title="Toggle Sidebar">
<!-- Panah kiri ( artinya tutup sidebar) -->
<svg class="toggle-arrow" viewBox="0 0 24 24"> <svg class="toggle-arrow" viewBox="0 0 24 24">
<polyline points="15 18 9 12 15 6"></polyline> <polyline points="15 18 9 12 15 6"></polyline>
</svg> </svg>
</button> </button>
<!-- MAIN -->
<div class="main" id="mainContent"> <div class="main" id="mainContent">
<!-- TOPBAR -->
<header class="topbar"> <header class="topbar">
<div class="topbar-left"> <div class="topbar-left">
👋 Hai, {{ Auth::guard('siswa')->user()->nama ?? 'Siswa' }} 👋 Hai, {{ Auth::guard('siswa')->user()->nama ?? 'Siswa' }}
@ -268,62 +212,47 @@ class="sidebar-link {{ request()->routeIs('siswa.leaderboard*') ? 'active' : ''
<div class="topbar-right"> <div class="topbar-right">
<img src="{{ asset('images/icon/sidebar/notif.png') }}" class="topbar-icon" alt="Notification"> <img src="{{ asset('images/icon/sidebar/notif.png') }}" class="topbar-icon" alt="Notification">
<img src="{{ asset('images/icon/sidebar/profil.png') }}" class="topbar-icon" alt="Profile">
{{-- Foto profil atau icon default --}}
@php $siswa = Auth::guard('siswa')->user(); @endphp
@if($siswa->foto_profil)
<a href="{{ route('siswa.profile.edit') }}">
<img src="{{ Storage::url($siswa->foto_profil) }}"
class="topbar-avatar" alt="Profil">
</a>
@else
<a href="{{ route('siswa.profile.edit') }}" class="topbar-avatar-icon">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
</svg>
</a>
@endif
</div> </div>
</header> </header>
<!-- CONTENT -->
<main class="content"> <main class="content">
@yield('content') @yield('content')
</main> </main>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
const sidebar = document.getElementById('mainSidebar'); const sidebar = document.getElementById('mainSidebar');
const toggleBtn = document.getElementById('sidebarToggleBtn'); const toggleBtn = document.getElementById('sidebarToggleBtn');
const SIDEBAR_W = 260; // harus sama dengan CSS .sidebar width const SIDEBAR_W = 260;
function updateTogglePosition(isCollapsed) { function updateTogglePosition(isCollapsed) {
if (isCollapsed) { toggleBtn.style.left = isCollapsed ? '0px' : SIDEBAR_W + 'px';
toggleBtn.style.left = '0px'; isCollapsed ? toggleBtn.classList.add('collapsed') : toggleBtn.classList.remove('collapsed');
toggleBtn.classList.add('collapsed');
} else {
toggleBtn.style.left = SIDEBAR_W + 'px';
toggleBtn.classList.remove('collapsed');
}
} }
toggleBtn.addEventListener('click', function () { toggleBtn.addEventListener('click', function () {
const isCurrentlyCollapsed = sidebar.classList.contains('collapsed'); const isCollapsed = sidebar.classList.contains('collapsed');
sidebar.classList.toggle('collapsed');
if (isCurrentlyCollapsed) { updateTogglePosition(!isCollapsed);
// Buka sidebar localStorage.setItem('sidebarOpen', isCollapsed ? 'true' : 'false');
sidebar.classList.remove('collapsed');
updateTogglePosition(false);
localStorage.setItem('sidebarOpen', 'true');
} else {
// Tutup sidebar
sidebar.classList.add('collapsed');
updateTogglePosition(true);
localStorage.setItem('sidebarOpen', 'false');
}
}); });
// Tidak perlu restore dari localStorage karena kita mau selalu collapsed saat login baru.
// Tapi kalau mau ingat preferensi user dalam satu sesi browsing, uncomment ini:
/*
window.addEventListener('DOMContentLoaded', () => {
const saved = localStorage.getItem('sidebarOpen');
if (saved === 'true') {
sidebar.classList.remove('collapsed');
updateTogglePosition(false);
}
});
*/
</script> </script>
@stack('scripts') @stack('scripts')

View File

@ -7,137 +7,62 @@
.page-title { font-size: 28px; font-weight: 800; margin-top: -20px; margin-bottom: 6px; } .page-title { font-size: 28px; font-weight: 800; margin-top: -20px; margin-bottom: 6px; }
.page-subtitle { font-size: 14px; color: #64748b; margin-bottom: 24px; } .page-subtitle { font-size: 14px; color: #64748b; margin-bottom: 24px; }
/* Podium top 3 */ .podium-wrap { display: flex; align-items: flex-end; justify-content: center; gap: 12px; margin-bottom: 32px; }
.podium-wrap { .podium-item { display: flex; flex-direction: column; align-items: center; gap: 8px; }
display: flex;
align-items: flex-end;
justify-content: center;
gap: 12px;
margin-bottom: 32px;
}
.podium-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.podium-avatar { .podium-avatar {
width: 56px;
height: 56px;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex; align-items: center; justify-content: center;
align-items: center; font-size: 22px; font-weight: 800; color: white;
justify-content: center; position: relative; overflow: hidden; flex-shrink: 0;
font-size: 22px; width: 56px; height: 56px;
font-weight: 800;
color: white;
position: relative;
} }
.podium-crown { .podium-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
position: absolute; .podium-crown { position: absolute; top: -16px; font-size: 18px; z-index: 1; }
top: -16px;
font-size: 18px;
}
.rank-1 .podium-avatar { background: linear-gradient(135deg, #f59e0b, #d97706); width: 68px; height: 68px; font-size: 26px; } .rank-1 .podium-avatar { background: linear-gradient(135deg,#f59e0b,#d97706); width:68px; height:68px; font-size:26px; }
.rank-2 .podium-avatar { background: linear-gradient(135deg, #94a3b8, #64748b); } .rank-2 .podium-avatar { background: linear-gradient(135deg,#94a3b8,#64748b); }
.rank-3 .podium-avatar { background: linear-gradient(135deg, #f97316, #ea580c); } .rank-3 .podium-avatar { background: linear-gradient(135deg,#f97316,#ea580c); }
.podium-name { font-size: 13px; font-weight: 700; color: #1e293b; text-align: center; max-width: 90px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .podium-name { font-size:13px; font-weight:700; color:#1e293b; text-align:center; max-width:90px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.podium-exp { font-size: 12px; color: #64748b; } .podium-exp { font-size:12px; color:#64748b; }
.podium-bar { .podium-bar { border-radius:12px 12px 0 0; width:80px; display:flex; align-items:center; justify-content:center; font-size:20px; font-weight:800; color:white; }
border-radius: 12px 12px 0 0; .rank-1 .podium-bar { height:80px; background:linear-gradient(135deg,#f59e0b,#d97706); }
width: 80px; .rank-2 .podium-bar { height:60px; background:linear-gradient(135deg,#94a3b8,#64748b); }
display: flex; .rank-3 .podium-bar { height:44px; background:linear-gradient(135deg,#f97316,#ea580c); }
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 800;
color: white;
}
.rank-1 .podium-bar { height: 80px; background: linear-gradient(135deg, #f59e0b, #d97706); } .my-rank-banner { background:linear-gradient(135deg,#667eea,#764ba2); border-radius:16px; padding:16px 20px; color:white; display:flex; align-items:center; gap:16px; margin-bottom:20px; }
.rank-2 .podium-bar { height: 60px; background: linear-gradient(135deg, #94a3b8, #64748b); } .my-rank-avatar { width:48px; height:48px; border-radius:50%; object-fit:cover; border:2px solid rgba(255,255,255,0.5); flex-shrink:0; }
.rank-3 .podium-bar { height: 44px; background: linear-gradient(135deg, #f97316, #ea580c); } .my-rank-avatar-placeholder { width:48px; height:48px; border-radius:50%; background:rgba(255,255,255,0.2); display:flex; align-items:center; justify-content:center; font-size:20px; font-weight:800; flex-shrink:0; }
.my-rank-num { font-size:36px; font-weight:800; line-height:1; }
.my-rank-info { flex:1; }
.my-rank-label { font-size:12px; opacity:0.8; margin-bottom:2px; }
.my-rank-nama { font-size:16px; font-weight:700; }
.my-rank-exp { font-size:13px; opacity:0.9; }
/* My rank banner */ .custom-card { background:white; border-radius:20px; border:2px solid #e5e5e5; padding:22px; }
.my-rank-banner { .section-title { font-size:15px; font-weight:700; color:#1e293b; margin-bottom:16px; }
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 16px;
padding: 16px 20px;
color: white;
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.my-rank-num { .lb-row { display:flex; align-items:center; gap:12px; padding:10px 14px; border-radius:12px; margin-bottom:8px; transition:background 0.15s; }
font-size: 36px; .lb-row:hover { background:#f8fafc; }
font-weight: 800; .lb-row.highlight { background:#f0eeff; border:2px solid #c4b5fd; }
line-height: 1;
}
.my-rank-info { flex: 1; } .lb-rank { width:32px; height:32px; border-radius:50%; background:#e2e8f0; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#64748b; flex-shrink:0; }
.my-rank-label { font-size: 12px; opacity: 0.8; margin-bottom: 2px; } .lb-rank.gold { background:#fef3c7; color:#d97706; }
.my-rank-nama { font-size: 16px; font-weight: 700; } .lb-rank.silver { background:#f1f5f9; color:#64748b; }
.my-rank-exp { font-size: 13px; opacity: 0.9; } .lb-rank.bronze { background:#ffedd5; color:#ea580c; }
/* Tabel */ .lb-avatar { width:36px; height:36px; border-radius:50%; object-fit:cover; flex-shrink:0; border:2px solid #e2e8f0; }
.custom-card { background: white; border-radius: 20px; border: 2px solid #e5e5e5; padding: 22px; } .lb-avatar-placeholder { width:36px; height:36px; border-radius:50%; background:#e6f0ff; display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#2b8ef3; flex-shrink:0; }
.section-title { font-size: 15px; font-weight: 700; color: #1e293b; margin-bottom: 16px; }
.lb-row { .lb-nama { flex:1; font-size:14px; font-weight:600; color:#1e293b; }
display: flex; .lb-nisn { font-size:12px; color:#94a3b8; }
align-items: center; .lb-exp { font-size:14px; font-weight:700; color:#667eea; }
gap: 14px;
padding: 12px 14px;
border-radius: 12px;
margin-bottom: 8px;
transition: background 0.15s;
}
.lb-row:hover { background: #f8fafc; } .semester-badge { display:inline-block; background:#f0eeff; color:#667eea; font-size:12px; font-weight:700; padding:4px 12px; border-radius:99px; margin-bottom:20px; }
.lb-row.highlight { background: #f0eeff; border: 2px solid #c4b5fd; } .empty-state { text-align:center; padding:40px 20px; color:#94a3b8; }
.lb-rank {
width: 32px;
height: 32px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
color: #64748b;
flex-shrink: 0;
}
.lb-rank.gold { background: #fef3c7; color: #d97706; }
.lb-rank.silver { background: #f1f5f9; color: #64748b; }
.lb-rank.bronze { background: #ffedd5; color: #ea580c; }
.lb-nama { flex: 1; font-size: 14px; font-weight: 600; color: #1e293b; }
.lb-nisn { font-size: 12px; color: #94a3b8; }
.lb-exp { font-size: 14px; font-weight: 700; color: #667eea; }
.semester-badge {
display: inline-block;
background: #f0eeff;
color: #667eea;
font-size: 12px;
font-weight: 700;
padding: 4px 12px;
border-radius: 99px;
margin-bottom: 20px;
}
.empty-state { text-align: center; padding: 40px 20px; color: #94a3b8; }
</style> </style>
@endpush @endpush
@ -148,9 +73,7 @@
<h3 class="page-title">🏅 Leaderboard</h3> <h3 class="page-title">🏅 Leaderboard</h3>
<p class="page-subtitle">Peringkat siswa berdasarkan total EXP yang dikumpulkan.</p> <p class="page-subtitle">Peringkat siswa berdasarkan total EXP yang dikumpulkan.</p>
<span class="semester-badge"> <span class="semester-badge">Semester {{ $semester }} · {{ $tahunAjaran }}</span>
Semester {{ $semester }} · {{ $tahunAjaran }}
</span>
@if($leaderboard->isEmpty()) @if($leaderboard->isEmpty())
<div class="empty-state"> <div class="empty-state">
@ -160,9 +83,8 @@
</div> </div>
@else @else
{{-- Podium Top 3 --}}
@php @php
$top3 = $leaderboard->take(3); $top3 = $leaderboard->take(3);
$first = $top3->firstWhere('ranking', 1); $first = $top3->firstWhere('ranking', 1);
$second = $top3->firstWhere('ranking', 2); $second = $top3->firstWhere('ranking', 2);
$third = $top3->firstWhere('ranking', 3); $third = $top3->firstWhere('ranking', 3);
@ -171,31 +93,44 @@
@if($first) @if($first)
<div class="podium-wrap"> <div class="podium-wrap">
{{-- Rank 2 --}}
@if($second) @if($second)
<div class="podium-item rank-2"> <div class="podium-item rank-2">
<div class="podium-avatar">{{ strtoupper(substr($second['nama'], 0, 1)) }}</div> <div class="podium-avatar">
@if(!empty($second['foto_profil']))
<img src="{{ Storage::url($second['foto_profil']) }}" alt="">
@else
{{ strtoupper(substr($second['nama'], 0, 1)) }}
@endif
</div>
<div class="podium-name">{{ $second['nama'] }}</div> <div class="podium-name">{{ $second['nama'] }}</div>
<div class="podium-exp"> {{ number_format($second['exp']) }}</div> <div class="podium-exp"> {{ number_format($second['exp']) }}</div>
<div class="podium-bar">2</div> <div class="podium-bar">2</div>
</div> </div>
@endif @endif
{{-- Rank 1 --}}
<div class="podium-item rank-1"> <div class="podium-item rank-1">
<div class="podium-avatar"> <div class="podium-avatar">
<span class="podium-crown">👑</span> <span class="podium-crown">👑</span>
{{ strtoupper(substr($first['nama'], 0, 1)) }} @if(!empty($first['foto_profil']))
<img src="{{ Storage::url($first['foto_profil']) }}" alt="">
@else
{{ strtoupper(substr($first['nama'], 0, 1)) }}
@endif
</div> </div>
<div class="podium-name">{{ $first['nama'] }}</div> <div class="podium-name">{{ $first['nama'] }}</div>
<div class="podium-exp"> {{ number_format($first['exp']) }}</div> <div class="podium-exp"> {{ number_format($first['exp']) }}</div>
<div class="podium-bar">1</div> <div class="podium-bar">1</div>
</div> </div>
{{-- Rank 3 --}}
@if($third) @if($third)
<div class="podium-item rank-3"> <div class="podium-item rank-3">
<div class="podium-avatar">{{ strtoupper(substr($third['nama'], 0, 1)) }}</div> <div class="podium-avatar">
@if(!empty($third['foto_profil']))
<img src="{{ Storage::url($third['foto_profil']) }}" alt="">
@else
{{ strtoupper(substr($third['nama'], 0, 1)) }}
@endif
</div>
<div class="podium-name">{{ $third['nama'] }}</div> <div class="podium-name">{{ $third['nama'] }}</div>
<div class="podium-exp"> {{ number_format($third['exp']) }}</div> <div class="podium-exp"> {{ number_format($third['exp']) }}</div>
<div class="podium-bar">3</div> <div class="podium-bar">3</div>
@ -205,9 +140,13 @@
</div> </div>
@endif @endif
{{-- My Rank Banner --}}
@if($myRank) @if($myRank)
<div class="my-rank-banner"> <div class="my-rank-banner">
@if(!empty($myRank['foto_profil']))
<img src="{{ Storage::url($myRank['foto_profil']) }}" class="my-rank-avatar" alt="">
@else
<div class="my-rank-avatar-placeholder">{{ strtoupper(substr($myRank['nama'], 0, 1)) }}</div>
@endif
<div class="my-rank-num">#{{ $myRank['ranking'] }}</div> <div class="my-rank-num">#{{ $myRank['ranking'] }}</div>
<div class="my-rank-info"> <div class="my-rank-info">
<div class="my-rank-label">Posisimu saat ini</div> <div class="my-rank-label">Posisimu saat ini</div>
@ -218,16 +157,13 @@
</div> </div>
@endif @endif
{{-- Tabel semua --}}
<div class="custom-card"> <div class="custom-card">
<p class="section-title">📋 Semua Peringkat</p> <p class="section-title">📋 Semua Peringkat</p>
@foreach($leaderboard as $item) @foreach($leaderboard as $item)
@php @php
$isMe = $item['id_siswa'] === $siswaLogin->id_siswa; $isMe = $item['id_siswa'] === $siswaLogin->id_siswa;
$rankClass = match($item['ranking']) { $rankClass = match($item['ranking']) { 1=>'gold', 2=>'silver', 3=>'bronze', default=>'' };
1 => 'gold', 2 => 'silver', 3 => 'bronze', default => ''
};
@endphp @endphp
<div class="lb-row {{ $isMe ? 'highlight' : '' }}"> <div class="lb-row {{ $isMe ? 'highlight' : '' }}">
<div class="lb-rank {{ $rankClass }}"> <div class="lb-rank {{ $rankClass }}">
@ -237,10 +173,19 @@
@else {{ $item['ranking'] }} @else {{ $item['ranking'] }}
@endif @endif
</div> </div>
@if(!empty($item['foto_profil']))
<img src="{{ Storage::url($item['foto_profil']) }}" class="lb-avatar" alt="">
@else
<div class="lb-avatar-placeholder">{{ strtoupper(substr($item['nama'], 0, 1)) }}</div>
@endif
<div style="flex:1"> <div style="flex:1">
<div class="lb-nama"> <div class="lb-nama">
{{ $item['nama'] }} {{ $item['nama'] }}
@if($isMe) <span style="background:#c4b5fd;color:#4c1d95;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:700;margin-left:4px">Kamu</span> @endif @if($isMe)
<span style="background:#c4b5fd;color:#4c1d95;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:700;margin-left:4px">Kamu</span>
@endif
</div> </div>
<div class="lb-nisn">{{ $item['nisn'] }}</div> <div class="lb-nisn">{{ $item['nisn'] }}</div>
</div> </div>

View File

@ -0,0 +1,255 @@
@extends('siswa.layouts.app')
@section('title', 'Edit Profil')
@push('styles')
<style>
.page-title { font-size: 24px; font-weight: 800; margin-bottom: 6px; }
.page-subtitle { font-size: 14px; color: #64748b; margin-bottom: 28px; }
.profile-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 32px;
max-width: 560px;
}
/* Foto profil */
.foto-wrap {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 32px;
padding-bottom: 28px;
border-bottom: 1px solid #f1f5f9;
}
.foto-preview {
width: 88px; height: 88px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #e6f0ff;
flex-shrink: 0;
}
.foto-placeholder {
width: 88px; height: 88px;
border-radius: 50%;
background: #e6f0ff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 3px solid #e6f0ff;
}
.foto-placeholder svg { width: 40px; height: 40px; color: #2b8ef3; }
.foto-info p { font-size: 13px; color: #64748b; margin: 0 0 10px; }
.btn-upload {
display: inline-block;
background: #e6f0ff;
color: #2b8ef3;
font-size: 13px;
font-weight: 700;
padding: 8px 18px;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s;
border: none;
}
.btn-upload:hover { background: #dbeeff; }
/* Form fields */
.field-group { margin-bottom: 18px; }
.field-label {
display: block;
font-size: 12px;
font-weight: 700;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.field-label span {
color: #94a3b8;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
margin-left: 4px;
}
.field-input {
width: 100%;
background: #f8fafc;
border: 1.5px solid #e2e8f0;
border-radius: 12px;
padding: 11px 14px;
font-size: 14px;
font-family: 'Poppins', sans-serif;
color: #1e293b;
outline: none;
transition: all 0.2s;
}
.field-input:focus {
border-color: #2b8ef3;
background: white;
box-shadow: 0 0 0 3px rgba(43,142,243,0.1);
}
.field-input:disabled {
background: #f1f5f9;
color: #94a3b8;
cursor: not-allowed;
}
.field-hint { font-size: 11px; color: #94a3b8; margin-top: 4px; }
.divider {
border: none;
border-top: 1px solid #f1f5f9;
margin: 24px 0;
}
.btn-save {
background: #2b8ef3;
color: white;
border: none;
border-radius: 12px;
padding: 12px 28px;
font-size: 14px;
font-weight: 700;
font-family: 'Poppins', sans-serif;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 14px rgba(43,142,243,0.3);
}
.btn-save:hover { background: #1a7ae0; transform: translateY(-1px); }
.alert-success {
background: #f0fdf4;
border: 1.5px solid #86efac;
color: #166534;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
margin-bottom: 20px;
}
.alert-error {
background: #fef2f2;
border: 1.5px solid #fca5a5;
color: #991b1b;
border-radius: 12px;
padding: 12px 16px;
font-size: 13px;
margin-bottom: 20px;
}
</style>
@endpush
@section('content')
@php $siswa = Auth::guard('siswa')->user(); @endphp
<h3 class="page-title">Edit Profil</h3>
<p class="page-subtitle">Perbarui foto profil dan password akunmu.</p>
@if(session('success'))
<div class="alert-success"> {{ session('success') }}</div>
@endif
@if($errors->any())
<div class="alert-error">
<ul style="margin:0;padding-left:16px">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('siswa.profile.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="profile-card">
{{-- Foto Profil --}}
<div class="foto-wrap">
@if($siswa->foto_profil)
<img src="{{ Storage::url($siswa->foto_profil) }}" class="foto-preview" id="foto-preview" alt="Foto Profil">
@else
<div class="foto-placeholder" id="foto-placeholder">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
</svg>
</div>
<img src="" class="foto-preview" id="foto-preview" alt="" style="display:none">
@endif
<div class="foto-info">
<p>Format: JPG, PNG, WEBP. Maks. 2MB.</p>
<label for="foto_profil" class="btn-upload">Pilih Foto</label>
<input type="file" name="foto_profil" id="foto_profil" accept="image/*" style="display:none">
</div>
</div>
{{-- Info tidak bisa diubah --}}
<div class="field-group">
<label class="field-label">NISN <span>(tidak dapat diubah)</span></label>
<input type="text" class="field-input" value="{{ $siswa->nisn }}" disabled>
</div>
<div class="field-group">
<label class="field-label">Nama Lengkap <span>(tidak dapat diubah)</span></label>
<input type="text" class="field-input" value="{{ $siswa->nama }}" disabled>
</div>
<hr class="divider">
{{-- Password --}}
<div class="field-group">
<label class="field-label">Password Baru <span>(kosongkan jika tidak ingin mengubah)</span></label>
<input type="password" name="password" class="field-input"
placeholder="Masukkan password baru" autocomplete="new-password">
</div>
<div class="field-group">
<label class="field-label">Konfirmasi Password Baru</label>
<input type="password" name="password_confirmation" class="field-input"
placeholder="Ulangi password baru" autocomplete="new-password">
</div>
<div style="margin-top:8px">
<button type="submit" class="btn-save">Simpan Perubahan</button>
</div>
</div>
</form>
@endsection
@push('scripts')
<script>
document.getElementById('foto_profil').addEventListener('change', function () {
const file = this.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
const preview = document.getElementById('foto-preview');
const placeholder = document.getElementById('foto-placeholder');
preview.src = url;
preview.style.display = 'block';
if (placeholder) placeholder.style.display = 'none';
});
</script>
@endpush

View File

@ -13,16 +13,16 @@
use App\Http\Controllers\Admin\MapelController as AdminMapelController; use App\Http\Controllers\Admin\MapelController as AdminMapelController;
use App\Http\Controllers\Admin\ChallengeController as AdminChallengeController; use App\Http\Controllers\Admin\ChallengeController as AdminChallengeController;
use App\Http\Controllers\Admin\LeaderboardController as AdminLeaderboardController; use App\Http\Controllers\Admin\LeaderboardController as AdminLeaderboardController;
use App\Http\Controllers\Admin\ProfileController as AdminProfileController;
use App\Http\Controllers\Admin\MateriTugasController as AdminMateriTugasController; use App\Http\Controllers\Admin\MateriTugasController as AdminMateriTugasController;
// GURU CONTROLLERS // GURU CONTROLLERS
use App\Http\Controllers\Guru\LoginController as GuruLoginController; use App\Http\Controllers\Guru\LoginController as GuruLoginController;
use App\Http\Controllers\Guru\DashboardController as GuruDashboardController; use App\Http\Controllers\Guru\DashboardController as GuruDashboardController;
use App\Http\Controllers\Guru\GuruController as GuruGuruController; use App\Http\Controllers\Guru\GuruController as GuruGuruController;
use App\Http\Controllers\Guru\KelasController as GuruKelasController; use App\Http\Controllers\Guru\KelasController as GuruKelasController;
use App\Http\Controllers\Guru\SiswaController as GuruSiswaController; use App\Http\Controllers\Guru\SiswaController as GuruSiswaController;
use App\Http\Controllers\Guru\ProfilController as GuruProfilController; use App\Http\Controllers\Guru\ProfileController as GuruProfileController;
use App\Http\Controllers\Guru\MapelController as GuruMapelController; use App\Http\Controllers\Guru\MapelController as GuruMapelController;
use App\Http\Controllers\Guru\LeaderboardController as GuruLeaderboardController; use App\Http\Controllers\Guru\LeaderboardController as GuruLeaderboardController;
@ -33,6 +33,7 @@
use App\Http\Controllers\Siswa\TugasController as SiswaTugasController; use App\Http\Controllers\Siswa\TugasController as SiswaTugasController;
use App\Http\Controllers\Siswa\ChallengeController as SiswaChallengeController; use App\Http\Controllers\Siswa\ChallengeController as SiswaChallengeController;
use App\Http\Controllers\Siswa\LeaderboardController as SiswaLeaderboardController; use App\Http\Controllers\Siswa\LeaderboardController as SiswaLeaderboardController;
use App\Http\Controllers\Siswa\ProfileController as SiswaProfileController;
// ==================== // ====================
// LANDING PAGE // LANDING PAGE
@ -87,9 +88,8 @@
return view('admin.notif'); return view('admin.notif');
})->name('notif'); })->name('notif');
Route::get('/profil', function () { Route::get('/profile', [AdminProfileController::class, 'edit'])->name('profile.edit');
return view('admin.profil'); Route::put('/profile', [AdminProfileController::class, 'update'])->name('profile.update');
})->name('profil');
// ── GURU ────────────────────────────────────────────── // ── GURU ──────────────────────────────────────────────
Route::get('/guru/kelas-by-mapel', [AdminGuruController::class, 'getKelasByMapel']) Route::get('/guru/kelas-by-mapel', [AdminGuruController::class, 'getKelasByMapel'])
@ -111,7 +111,6 @@
Route::delete('/tugas/{id}', [AdminMateriTugasController::class, 'destroyTugas'])->name('tugas.destroy'); Route::delete('/tugas/{id}', [AdminMateriTugasController::class, 'destroyTugas'])->name('tugas.destroy');
// ── CHALLENGE ───────────────────────────────────────── // ── CHALLENGE ─────────────────────────────────────────
// WAJIB di atas Route::resource agar tidak konflik dengan {challenge} wildcard
Route::get('/challenge/{id}/edit-data', [AdminChallengeController::class, 'editData']) Route::get('/challenge/{id}/edit-data', [AdminChallengeController::class, 'editData'])
->name('challenge.editData'); ->name('challenge.editData');
Route::resource('challenge', AdminChallengeController::class); Route::resource('challenge', AdminChallengeController::class);
@ -155,10 +154,8 @@
Route::get('/tugas/{id}/detail', [GuruMapelController::class, 'detailTugas'])->name('tugas.detail'); Route::get('/tugas/{id}/detail', [GuruMapelController::class, 'detailTugas'])->name('tugas.detail');
Route::delete('/tugas/{id}', [GuruMapelController::class, 'destroyTugas'])->name('tugas.destroy'); Route::delete('/tugas/{id}', [GuruMapelController::class, 'destroyTugas'])->name('tugas.destroy');
Route::get('/profile', [GuruProfileController::class, 'edit'])->name('profile.edit');
// Profil (Edit) Route::put('/profile', [GuruProfileController::class, 'update'])->name('profile.update');
Route::get('/profil', [GuruProfilController::class, 'show'])->name('profil.show');
Route::put('/profil', [GuruProfilController::class, 'update'])->name('profil.update');
// LOGOUT GURU // LOGOUT GURU
Route::post('/logout', [GuruLoginController::class, 'logout'])->name('logout'); Route::post('/logout', [GuruLoginController::class, 'logout'])->name('logout');
@ -188,6 +185,10 @@
//LEADERBOARD SISWA //LEADERBOARD SISWA
Route::get('/leaderboard', [SiswaLeaderboardController::class, 'index'])->name('leaderboard.index'); Route::get('/leaderboard', [SiswaLeaderboardController::class, 'index'])->name('leaderboard.index');
//PROFILE SISWA
Route::get('/profile', [SiswaProfileController::class, 'edit'])->name('profile.edit');
Route::put('/profile', [SiswaProfileController::class, 'update'])->name('profile.update');
// LOGOUT SISWA // LOGOUT SISWA
Route::post('/logout', [SiswaLoginController::class, 'logout'])->name('logout'); Route::post('/logout', [SiswaLoginController::class, 'logout'])->name('logout');