feat: Inertia profile settings page, Studio edit redesign, EGS, Nova UI components\n\n- Redesign /dashboard/profile as Inertia React page (Settings/ProfileEdit)\n with SettingsLayout sidebar, Nova UI components (TextInput, Textarea,\n Toggle, Select, RadioGroup, Modal, Button), avatar drag-and-drop,\n password change, and account deletion sections\n- Redesign Studio artwork edit page with two-column layout, Nova components,\n integrated TagPicker, and version history modal\n- Add shared MarkdownEditor component\n- Add Early-Stage Growth System (EGS): SpotlightEngine, FeedBlender,\n GridFiller, AdaptiveTimeWindow, ActivityLayer, admin panel\n- Fix upload category/tag persistence (V1+V2 paths)\n- Fix tag source enum, category tree display, binding resolution\n- Add settings.jsx Vite entry, settings.blade.php wrapper\n- Update ProfileController with JSON response support for API calls\n- Various route fixes (profile.edit, toolbar settings link)"

This commit is contained in:
2026-03-03 20:57:43 +01:00
parent dc51d65440
commit b9c2d8597d
114 changed files with 8760 additions and 693 deletions

View File

@@ -121,7 +121,94 @@ class ProfileController extends Controller
]);
}
public function update(ProfileUpdateRequest $request, \App\Services\AvatarService $avatarService): RedirectResponse
/**
* Inertia-powered profile edit page (Settings/ProfileEdit).
*/
public function editSettings(Request $request)
{
$user = $request->user();
// Parse birth date parts
$birthDay = null;
$birthMonth = null;
$birthYear = null;
// Merge modern user_profiles data
$profileData = [];
try {
if (Schema::hasTable('user_profiles')) {
$profile = DB::table('user_profiles')->where('user_id', $user->id)->first();
if ($profile) {
$profileData = (array) $profile;
if (isset($profile->website)) $user->homepage = $profile->website;
if (isset($profile->about)) $user->about_me = $profile->about;
if (isset($profile->birthdate)) $user->birth = $profile->birthdate;
if (isset($profile->gender)) $user->gender = $profile->gender;
if (isset($profile->country_code)) $user->country_code = $profile->country_code;
if (isset($profile->signature)) $user->signature = $profile->signature;
if (isset($profile->description)) $user->description = $profile->description;
if (isset($profile->mlist)) $user->mlist = $profile->mlist;
if (isset($profile->friend_upload_notice)) $user->friend_upload_notice = $profile->friend_upload_notice;
if (isset($profile->auto_post_upload)) $user->auto_post_upload = $profile->auto_post_upload;
}
}
} catch (\Throwable $e) {}
if (!empty($user->birth)) {
try {
$dt = \Carbon\Carbon::parse($user->birth);
$birthDay = $dt->format('d');
$birthMonth = $dt->format('m');
$birthYear = $dt->format('Y');
} catch (\Throwable $e) {}
}
// Country list
$countries = collect();
try {
if (Schema::hasTable('country_list')) {
$countries = DB::table('country_list')->orderBy('country_name')->get();
} elseif (Schema::hasTable('countries')) {
$countries = DB::table('countries')->orderBy('name')->get();
}
} catch (\Throwable $e) {}
// Avatar URL
$avatarHash = $profileData['avatar_hash'] ?? $user->icon ?? null;
$avatarUrl = !empty($avatarHash)
? AvatarUrl::forUser((int) $user->id, $avatarHash, 128)
: AvatarUrl::default();
return Inertia::render('Settings/ProfileEdit', [
'user' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
'name' => $user->name,
'homepage' => $user->homepage ?? $user->website ?? null,
'about_me' => $user->about_me ?? $user->about ?? null,
'signature' => $user->signature ?? null,
'description' => $user->description ?? null,
'gender' => $user->gender ?? null,
'country_code' => $user->country_code ?? null,
'mlist' => $user->mlist ?? false,
'friend_upload_notice' => $user->friend_upload_notice ?? false,
'auto_post_upload' => $user->auto_post_upload ?? false,
'username_changed_at' => $user->username_changed_at,
],
'avatarUrl' => $avatarUrl,
'birthDay' => $birthDay,
'birthMonth' => $birthMonth,
'birthYear' => $birthYear,
'countries' => $countries->values(),
'flash' => [
'status' => session('status'),
'error' => session('error'),
],
])->rootView('settings');
}
public function update(ProfileUpdateRequest $request, \App\Services\AvatarService $avatarService): RedirectResponse|JsonResponse
{
$user = $request->user();
@@ -144,18 +231,22 @@ class ProfileController extends Controller
'current_username' => $currentUsername,
]);
return Redirect::back()->withErrors([
'username' => 'This username is too similar to a reserved name and requires manual approval.',
]);
$error = ['username' => ['This username is too similar to a reserved name and requires manual approval.']];
if ($request->expectsJson()) {
return response()->json(['errors' => $error], 422);
}
return Redirect::back()->withErrors($error);
}
$cooldownDays = (int) config('usernames.rename_cooldown_days', 90);
$isAdmin = method_exists($user, 'isAdmin') ? $user->isAdmin() : false;
if (! $isAdmin && $user->username_changed_at !== null && $user->username_changed_at->gt(now()->subDays($cooldownDays))) {
return Redirect::back()->withErrors([
'username' => "Username can only be changed once every {$cooldownDays} days.",
]);
$error = ['username' => ["Username can only be changed once every {$cooldownDays} days."]];
if ($request->expectsJson()) {
return response()->json(['errors' => $error], 422);
}
return Redirect::back()->withErrors($error);
}
$user->username = $incomingUsername;
@@ -234,6 +325,9 @@ class ProfileController extends Controller
try {
$avatarService->storeFromUploadedFile($user->id, $request->file('avatar'));
} catch (\Exception $e) {
if ($request->expectsJson()) {
return response()->json(['errors' => ['avatar' => ['Avatar processing failed: ' . $e->getMessage()]]], 422);
}
return Redirect::back()->with('error', 'Avatar processing failed: ' . $e->getMessage());
}
}
@@ -274,12 +368,17 @@ class ProfileController extends Controller
logger()->error('Profile update error: '.$e->getMessage());
}
if ($request->expectsJson()) {
return response()->json(['success' => true]);
}
return Redirect::route('dashboard.profile')->with('status', 'profile-updated');
}
public function destroy(Request $request): RedirectResponse
public function destroy(Request $request): RedirectResponse|JsonResponse
{
$request->validateWithBag('userDeletion', [
$bag = $request->expectsJson() ? 'default' : 'userDeletion';
$request->validateWithBag($bag, [
'password' => ['required', 'current_password'],
]);
@@ -292,10 +391,14 @@ class ProfileController extends Controller
$request->session()->invalidate();
$request->session()->regenerateToken();
if ($request->expectsJson()) {
return response()->json(['success' => true]);
}
return Redirect::to('/');
}
public function password(Request $request): RedirectResponse
public function password(Request $request): RedirectResponse|JsonResponse
{
$request->validate([
'current_password' => ['required', 'current_password'],
@@ -306,6 +409,10 @@ class ProfileController extends Controller
$user->password = Hash::make($request->input('password'));
$user->save();
if ($request->expectsJson()) {
return response()->json(['success' => true]);
}
return Redirect::route('dashboard.profile')->with('status', 'password-updated');
}