feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile

Forum:
- TipTap WYSIWYG editor with full toolbar
- @emoji-mart/react emoji picker (consistent with tweets)
- @mention autocomplete with user search API
- Fix PHP 8.4 parse errors in Blade templates
- Fix thread data display (paginator items)
- Align forum page widths to max-w-5xl

Discover:
- Extract shared _nav.blade.php partial
- Add missing nav links to for-you page
- Add Following link for authenticated users

Feed/Posts:
- Post model, controllers, policies, migrations
- Feed page components (PostComposer, FeedCard, etc)
- Post reactions, comments, saves, reports, sharing
- Scheduled publishing support
- Link preview controller

Profile:
- Profile page components (ProfileHero, ProfileTabs)
- Profile API controller

Uploads:
- Upload wizard enhancements
- Scheduled publish picker
- Studio status bar and readiness checklist
This commit is contained in:
2026-03-03 09:48:31 +01:00
parent 1266f81d35
commit dc51d65440
178 changed files with 14308 additions and 665 deletions

View File

@@ -25,6 +25,7 @@ use Illuminate\Support\Facades\Schema;
use Illuminate\View\View;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password as PasswordRule;
use Inertia\Inertia;
class ProfileController extends Controller
{
@@ -220,6 +221,10 @@ class ProfileController extends Controller
$profileUpdates['friend_upload_notice'] = filter_var($validated['notify'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
}
if (array_key_exists('auto_post_upload', $validated)) {
$profileUpdates['auto_post_upload'] = filter_var($validated['auto_post_upload'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
}
if (isset($validated['signature'])) $profileUpdates['signature'] = $validated['signature'];
if (isset($validated['description'])) $profileUpdates['description'] = $validated['description'];
@@ -498,24 +503,70 @@ class ProfileController extends Controller
} catch (\Throwable) {}
}
return response()->view('legacy::profile', [
'user' => $user,
'profile' => $profile,
'artworks' => $artworks,
'featuredArtworks' => $featuredArtworks,
'favourites' => $favourites,
// ── Normalise artworks for JSON serialisation ────────────────────
$artworkItems = collect($artworks->items())->values();
$artworkPayload = [
'data' => $artworkItems,
'next_cursor' => $artworks->nextCursor()?->encode(),
'has_more' => $artworks->hasMorePages(),
];
// ── Avatar URL on user object ────────────────────────────────────
$avatarUrl = AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash, 128);
// ── Auth context for JS ───────────────────────────────────────────
$authData = null;
if (Auth::check()) {
/** @var \App\Models\User $authUser */
$authUser = Auth::user();
$authAvatarUrl = AvatarUrl::forUser((int) $authUser->id, $authUser->profile?->avatar_hash, 64);
$authData = [
'user' => [
'id' => $authUser->id,
'username' => $authUser->username,
'name' => $authUser->name,
'avatar' => $authAvatarUrl,
],
];
}
$canonical = url('/@' . strtolower((string) ($user->username ?? '')));
return Inertia::render('Profile/ProfileShow', [
'user' => [
'id' => $user->id,
'username' => $user->username,
'name' => $user->name,
'avatar_url' => $avatarUrl,
'created_at' => $user->created_at?->toISOString(),
'last_visit_at' => $user->last_visit_at ? (string) $user->last_visit_at : null,
],
'profile' => $profile ? [
'about' => $profile->about ?? null,
'website' => $profile->website ?? null,
'country_code' => $profile->country_code ?? null,
'gender' => $profile->gender ?? null,
'birthdate' => $profile->birthdate ?? null,
'cover_image' => $profile->cover_image ?? null,
] : null,
'artworks' => $artworkPayload,
'featuredArtworks' => $featuredArtworks->values(),
'favourites' => $favourites->values(),
'stats' => $stats,
'socialLinks' => $socialLinks,
'followerCount' => $followerCount,
'recentFollowers' => $recentFollowers,
'recentFollowers' => $recentFollowers->values(),
'viewerIsFollowing' => $viewerIsFollowing,
'heroBgUrl' => $heroBgUrl,
'profileComments' => $profileComments,
'profileComments' => $profileComments->values(),
'countryName' => $countryName,
'isOwner' => $isOwner,
'page_title' => 'Profile: ' . ($user->username ?? $user->name ?? ''),
'page_canonical' => url('/@' . strtolower((string) ($user->username ?? ''))),
'auth' => $authData,
])->withViewData([
'page_title' => ($user->username ?? $user->name ?? 'User') . ' on Skinbase',
'page_canonical' => $canonical,
'page_meta_description' => 'View the profile of ' . ($user->username ?? $user->name) . ' on Skinbase.org — artworks, favourites and more.',
'og_image' => $avatarUrl,
]);
}
}