feat: implement user settings management with customizable notification styles, themes, and UI preferences
This commit is contained in:
parent
1030a215c3
commit
7c9493ad3d
19
app/Enums/NotifStyle.php
Normal file
19
app/Enums/NotifStyle.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Filament\Support\Contracts\HasLabel;
|
||||||
|
|
||||||
|
enum NotifStyle: string implements HasLabel
|
||||||
|
{
|
||||||
|
case Cheerful = 'cheerful';
|
||||||
|
case Formal = 'formal';
|
||||||
|
|
||||||
|
public function getLabel(): ?string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Cheerful => 'Ceria (Banyak Emoji) 🚀✨',
|
||||||
|
self::Formal => 'Formal & Profesional 💼',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Filament\Pages;
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use App\Enums\NotifStyle;
|
||||||
use App\Enums\Sex;
|
use App\Enums\Sex;
|
||||||
use App\Filament\Support\SystemNotification;
|
use App\Filament\Support\SystemNotification;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Filament\Auth\Pages\EditProfile;
|
use Filament\Auth\Pages\EditProfile;
|
||||||
|
use Filament\Forms\Components\ColorPicker;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Forms\Components\Radio;
|
use Filament\Forms\Components\Radio;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Schemas\Components\Grid;
|
use Filament\Schemas\Components\Grid;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
@ -157,33 +161,117 @@ public function form(Schema $schema): Schema
|
|||||||
->prefixIcon('heroicon-o-lock-closed'),
|
->prefixIcon('heroicon-o-lock-closed'),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
Section::make(SystemNotification::getMessage('Pengaturan Tampilan & UX 🎨✨', 'Preferensi Tampilan'))
|
||||||
|
->description(SystemNotification::getMessage('Personalisasi pengalaman aplikasi Anda agar lebih nyaman dan sesuai selera! 🌈🚀', 'Sesuaikan gaya bahasa, warna tema, dan jenis huruf aplikasi Anda.'))
|
||||||
|
->icon(SystemNotification::getNotifStyle() === NotifStyle::Cheerful ? 'heroicon-o-swatch' : 'heroicon-o-paint-brush')
|
||||||
|
->schema([
|
||||||
|
Grid::make(2)
|
||||||
|
->schema([
|
||||||
|
Select::make('settings.notif_style')
|
||||||
|
->label('Gaya Bahasa')
|
||||||
|
->options(NotifStyle::class)
|
||||||
|
->native(false)
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Select::make('settings.primary_color')
|
||||||
|
->label('Tema Warna')
|
||||||
|
->options([
|
||||||
|
'blue' => '🔵 Biru (Default)',
|
||||||
|
'sky' => '💎 Biru Langit',
|
||||||
|
'cyan' => '🌊 Biru Tosca (Cyan)',
|
||||||
|
'emerald' => '🟢 Emerald (Hijau)',
|
||||||
|
'teal' => '🍃 Teal (Hijau Keunguan)',
|
||||||
|
'lime' => '🍋 Lime (Hijah Muda)',
|
||||||
|
'amber' => '🟡 Amber (Kuning)',
|
||||||
|
'orange' => '🟠 Orange',
|
||||||
|
'rose' => '🔴 Rose (Merah)',
|
||||||
|
'fuchsia' => '🌸 Fuchsia (Pink Cerah)',
|
||||||
|
'violet' => '🟣 Violet (Ungu)',
|
||||||
|
'indigo' => '🌌 Indigo (Ungu Gelap)',
|
||||||
|
])
|
||||||
|
->placeholder('Pilih warna tema')
|
||||||
|
->native(false),
|
||||||
|
|
||||||
|
Select::make('settings.font')
|
||||||
|
->label('Jenis Huruf (Font)')
|
||||||
|
->options([
|
||||||
|
'Inter' => 'Inter (Modern)',
|
||||||
|
'Roboto' => 'Roboto (Clean)',
|
||||||
|
'Poppins' => 'Poppins (Rounder)',
|
||||||
|
'Outfit' => 'Outfit (Premium)',
|
||||||
|
'Montserrat' => 'Montserrat (Classic)',
|
||||||
|
'Lexend' => 'Lexend (Readable)',
|
||||||
|
])
|
||||||
|
->native(false),
|
||||||
|
|
||||||
|
Select::make('settings.content_width')
|
||||||
|
->label('Lebar Konten')
|
||||||
|
->options([
|
||||||
|
'full' => '↔️ Lebar Penuh (Full)',
|
||||||
|
'centered' => '🏢 Terpusat (Centered)',
|
||||||
|
])
|
||||||
|
->native(false),
|
||||||
|
|
||||||
|
Select::make('settings.border_radius')
|
||||||
|
->label('Radius Sudut (Border)')
|
||||||
|
->options([
|
||||||
|
'none' => '📐 Tegas (Sharp)',
|
||||||
|
'md' => '📱 Modern (Default)',
|
||||||
|
'lg' => '🎉 Rounded (Cheerful)',
|
||||||
|
'xl' => '🎈 Extra Round',
|
||||||
|
])
|
||||||
|
->native(false),
|
||||||
|
|
||||||
|
Toggle::make('settings.top_navigation')
|
||||||
|
->label('Navigasi Atas (Top Nav)')
|
||||||
|
->helperText('Pindahkan menu navigasi dari samping ke bagian atas layar.')
|
||||||
|
->onIcon('heroicon-m-window')
|
||||||
|
->offIcon('heroicon-m-arrow-top-right-on-square')
|
||||||
|
->inline(false),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function mutateFormDataBeforeFill(array $data): array
|
protected function mutateFormDataBeforeFill(array $data): array
|
||||||
{
|
{
|
||||||
$student = $this->getUser()->student;
|
$user = $this->getUser();
|
||||||
|
$student = $user->student;
|
||||||
|
$settings = $user->settings;
|
||||||
|
|
||||||
if ($student) {
|
if ($student) {
|
||||||
$data['student'] = $student->toArray();
|
$data['student'] = $student->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($settings) {
|
||||||
|
$data['settings'] = $settings->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||||
{
|
{
|
||||||
$studentData = $data['student'] ?? null;
|
$studentData = $data['student'] ?? null;
|
||||||
unset($data['student']);
|
$settingsData = $data['settings'] ?? null;
|
||||||
|
unset($data['student'], $data['settings']);
|
||||||
|
|
||||||
$record->update($data);
|
$record->update($data);
|
||||||
|
|
||||||
if ($studentData && $record->student) {
|
/** @var \App\Models\User $record */
|
||||||
|
if ($studentData && $record->student()->exists()) {
|
||||||
$record->student->update($studentData);
|
$record->student->update($studentData);
|
||||||
} elseif ($studentData) {
|
} elseif ($studentData) {
|
||||||
$record->student()->create($studentData);
|
$record->student()->create($studentData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($settingsData && $record->settings()->exists()) {
|
||||||
|
$record->settings->update($settingsData);
|
||||||
|
} elseif ($settingsData) {
|
||||||
|
$record->settings()->create($settingsData);
|
||||||
|
}
|
||||||
|
|
||||||
return $record;
|
return $record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
102
app/Http/Middleware/DynamicFilamentTheme.php
Normal file
102
app/Http/Middleware/DynamicFilamentTheme.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Support\Colors\Color;
|
||||||
|
use Filament\Support\Facades\FilamentColor;
|
||||||
|
use Filament\Support\Facades\FilamentView;
|
||||||
|
use Filament\View\PanelsRenderHook;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class DynamicFilamentTheme
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param Closure(Request): (Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = Filament::auth()->user();
|
||||||
|
|
||||||
|
if ($user && $user->settings) {
|
||||||
|
// Register Colors
|
||||||
|
FilamentColor::register([
|
||||||
|
'primary' => match ($user->settings->primary_color) {
|
||||||
|
'blue' => Color::Blue,
|
||||||
|
'sky' => Color::Sky,
|
||||||
|
'cyan' => Color::Cyan,
|
||||||
|
'emerald' => Color::Emerald,
|
||||||
|
'teal' => Color::Teal,
|
||||||
|
'lime' => Color::Lime,
|
||||||
|
'amber' => Color::Amber,
|
||||||
|
'orange' => Color::Orange,
|
||||||
|
'rose' => Color::Rose,
|
||||||
|
'fuchsia' => Color::Fuchsia,
|
||||||
|
'violet' => Color::Violet,
|
||||||
|
'indigo' => Color::Indigo,
|
||||||
|
default => Color::Blue,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Register Top Navigation
|
||||||
|
$panel = Filament::getCurrentOrDefaultPanel();
|
||||||
|
if ($panel && method_exists($panel, 'topNavigation')) {
|
||||||
|
$panel->topNavigation((bool) $user->settings->top_navigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register Font & UI Styles
|
||||||
|
$font = $user->settings->font ?? 'Inter';
|
||||||
|
$radius = match ($user->settings->border_radius ?? 'md') {
|
||||||
|
'none' => '0px',
|
||||||
|
'md' => '0.375rem',
|
||||||
|
'lg' => '0.5rem',
|
||||||
|
'xl' => '0.75rem',
|
||||||
|
'2xl' => '1rem',
|
||||||
|
default => '0.5rem',
|
||||||
|
};
|
||||||
|
$maxWidth = match ($user->settings->content_width ?? 'full') {
|
||||||
|
'centered' => '80rem',
|
||||||
|
default => 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
FilamentView::registerRenderHook(
|
||||||
|
PanelsRenderHook::HEAD_END,
|
||||||
|
fn () => new HtmlString("
|
||||||
|
<link rel='preconnect' href='https://fonts.googleapis.com'>
|
||||||
|
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>
|
||||||
|
<link href='https://fonts.googleapis.com/css2?family={$font}:wght@400;500;600;700&display=swap' rel='stylesheet'>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--font-family: '{$font}', sans-serif;
|
||||||
|
--c-border-radius: {$radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override Filament maximum width if centered */
|
||||||
|
.fi-main-ctn {
|
||||||
|
max-width: {$maxWidth} !important;
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-right: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply border radius to common elements */
|
||||||
|
.fi-section, .fi-btn, .fi-input, .fi-card, .fi-modal-window {
|
||||||
|
border-radius: {$radius} !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\IsActive;
|
use App\Enums\IsActive;
|
||||||
|
use App\Enums\NotifStyle;
|
||||||
use Filament\Models\Contracts\FilamentUser;
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||||
@ -63,4 +64,16 @@ public function student(): HasOne
|
|||||||
{
|
{
|
||||||
return $this->hasOne(Student::class);
|
return $this->hasOne(Student::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function settings(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(UserSetting::class)->withDefault([
|
||||||
|
'notif_style' => NotifStyle::Cheerful,
|
||||||
|
'primary_color' => 'amber', // Cheerful yellow
|
||||||
|
'font' => 'Inter',
|
||||||
|
'content_width' => 'full',
|
||||||
|
'border_radius' => 'lg',
|
||||||
|
'top_navigation' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
app/Models/UserSetting.php
Normal file
25
app/Models/UserSetting.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\NotifStyle;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class UserSetting extends Model
|
||||||
|
{
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'notif_style' => NotifStyle::class,
|
||||||
|
'top_navigation' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,8 @@
|
|||||||
use AchyutN\FilamentLogViewer\FilamentLogViewer;
|
use AchyutN\FilamentLogViewer\FilamentLogViewer;
|
||||||
use App\Filament\Pages\Auth\Login;
|
use App\Filament\Pages\Auth\Login;
|
||||||
use App\Filament\Pages\Profile;
|
use App\Filament\Pages\Profile;
|
||||||
|
use App\Http\Middleware\DynamicFilamentTheme;
|
||||||
|
use App\Models\User;
|
||||||
use BezhanSalleh\FilamentShield\FilamentShieldPlugin;
|
use BezhanSalleh\FilamentShield\FilamentShieldPlugin;
|
||||||
use DiogoGPinto\AuthUIEnhancer\AuthUIEnhancerPlugin;
|
use DiogoGPinto\AuthUIEnhancer\AuthUIEnhancerPlugin;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
@ -58,6 +60,7 @@ public function panel(Panel $panel): Panel
|
|||||||
SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
DisableBladeIconComponents::class,
|
DisableBladeIconComponents::class,
|
||||||
DispatchServingFilamentEvent::class,
|
DispatchServingFilamentEvent::class,
|
||||||
|
DynamicFilamentTheme::class,
|
||||||
])
|
])
|
||||||
->authMiddleware([
|
->authMiddleware([
|
||||||
Authenticate::class,
|
Authenticate::class,
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\NotifStyle;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('user_settings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
|
||||||
|
$table->enum('notif_style', NotifStyle::cases())->default(NotifStyle::Cheerful->value);
|
||||||
|
$table->string('primary_color')->nullable();
|
||||||
|
$table->string('font')->default('Inter');
|
||||||
|
$table->string('content_width')->default('full');
|
||||||
|
$table->string('border_radius')->default('lg');
|
||||||
|
$table->boolean('top_navigation')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user