feat: ship creator journey v2 and profile updates

This commit is contained in:
2026-04-12 21:42:07 +02:00
parent a2457f4e49
commit d5cff21ea2
335 changed files with 20147 additions and 1545 deletions

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ProfileUpdateRequest;
use App\Http\Requests\Settings\RequestEmailChangeRequest;
use App\Http\Requests\Settings\UpdateAccountSectionRequest;
use App\Http\Requests\Settings\UpdateContentPreferencesRequest;
use App\Http\Requests\Settings\UpdateNotificationsSectionRequest;
use App\Http\Requests\Settings\UpdatePersonalSectionRequest;
use App\Http\Requests\Settings\UpdateProfileSectionRequest;
@@ -35,10 +36,11 @@ use App\Services\FollowAnalyticsService;
use App\Services\LeaderboardService;
use App\Services\UserSuggestionService;
use App\Services\Countries\CountryCatalogService;
use App\Services\Maturity\ArtworkMaturityService;
use App\Services\ThumbnailPresenter;
use App\Services\ThumbnailService;
use App\Services\XPService;
use App\Services\UsernameApprovalService;
use App\Services\Profile\CreatorJourneyService;
use App\Services\UserStatsService;
use App\Support\AvatarUrl;
use App\Support\CoverUrl;
@@ -84,6 +86,7 @@ class ProfileController extends Controller
private readonly LeaderboardService $leaderboards,
private readonly CountryCatalogService $countryCatalog,
private readonly UserSuggestionService $userSuggestions,
private readonly CreatorJourneyService $creatorJourney,
)
{
}
@@ -312,6 +315,10 @@ class ProfileController extends Controller
$followerNotifications = (bool) ($profileData['follower_notifications'] ?? true);
$commentNotifications = (bool) ($profileData['comment_notifications'] ?? true);
$newsletter = (bool) ($profileData['newsletter'] ?? $profileData['mlist'] ?? $user->mlist ?? false);
$matureContentVisibility = (string) ($profileData['mature_content_visibility'] ?? config('maturity.viewer.default_mode', 'blur'));
$matureContentWarningEnabled = array_key_exists('mature_content_warning_enabled', $profileData)
? (bool) $profileData['mature_content_warning_enabled']
: (bool) config('maturity.viewer.default_warn_on_detail', true);
return Inertia::render('Settings/ProfileEdit', [
'user' => [
@@ -332,6 +339,8 @@ class ProfileController extends Controller
'follower_notifications' => $followerNotifications,
'comment_notifications' => $commentNotifications,
'newsletter' => $newsletter,
'mature_content_visibility' => $matureContentVisibility,
'mature_content_warning_enabled' => $matureContentWarningEnabled,
'last_username_change_at' => $user->last_username_change_at,
'username_changed_at' => $user->username_changed_at,
],
@@ -576,6 +585,18 @@ class ProfileController extends Controller
return $this->settingsResponse($request, 'Notification settings saved successfully.');
}
public function updateContentPreferencesSection(UpdateContentPreferencesRequest $request): RedirectResponse|JsonResponse
{
$validated = $request->validated();
$this->persistProfileUpdates((int) $request->user()->id, [
'mature_content_visibility' => (string) $validated['mature_content_visibility'],
'mature_content_warning_enabled' => (bool) $validated['mature_content_warning_enabled'],
]);
return $this->settingsResponse($request, 'Content preferences saved successfully.');
}
public function updateSecurityPassword(UpdateSecurityPasswordRequest $request): RedirectResponse|JsonResponse
{
$validated = $request->validated();
@@ -918,7 +939,7 @@ class ProfileController extends Controller
$perPage = 24;
// ── Artworks (cursor-paginated) ──────────────────────────────────────
$artworks = $this->artworkService->getArtworksByUser($user->id, $isOwner, $perPage)
$artworks = $this->artworkService->getArtworksByUser($user->id, $isOwner, $perPage, $viewer)
->through(function (Artwork $art) {
return (object) $this->mapArtworkCardPayload($art);
});
@@ -926,34 +947,38 @@ class ProfileController extends Controller
// ── Featured artworks for this user ─────────────────────────────────
$featuredArtworks = collect();
if (Schema::hasTable('artwork_features')) {
$featuredArtworks = DB::table('artwork_features as af')
->join('artworks as a', 'a.id', '=', 'af.artwork_id')
->where('a.user_id', $user->id)
$featuredQuery = Artwork::query()
->with([
'user:id,name,username,level,rank',
'user.profile:user_id,avatar_hash',
'group:id,name,slug,avatar_path',
'stats:artwork_id,views,downloads,favorites',
'categories' => function ($query) {
$query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
->with(['parent:id,parent_id,content_type_id,name,slug', 'contentType:id,slug,name']);
},
])
->join('artwork_features as af', 'af.artwork_id', '=', 'artworks.id')
->where('artworks.user_id', $user->id)
->where('af.is_active', true)
->whereNull('af.deleted_at')
->whereNull('a.deleted_at')
->where('a.is_public', true)
->where('a.is_approved', true)
->whereNull('artworks.deleted_at')
->where('artworks.is_public', true)
->where('artworks.is_approved', true)
->whereNotNull('artworks.published_at')
->select(['artworks.*', 'af.label as featured_label', 'af.featured_at as featured_slot_at'])
->orderByDesc('af.featured_at')
->limit(3)
->select([
'a.id', 'a.title as name', 'a.hash', 'a.thumb_ext',
'a.width', 'a.height', 'af.label', 'af.featured_at',
])
->limit(3);
app(ArtworkMaturityService::class)->applyViewerFilter($featuredQuery, $viewer);
$featuredArtworks = $featuredQuery
->get()
->map(function ($row) {
$thumbUrl = ($row->hash && $row->thumb_ext)
? ThumbnailService::fromHash($row->hash, $row->thumb_ext, 'md')
: '/images/placeholder.jpg';
return (object) [
'id' => $row->id,
'name' => $row->name,
'thumb' => $thumbUrl,
'label' => $row->label,
'featured_at' => $row->featured_at,
'width' => $row->width,
'height' => $row->height,
];
->map(function (Artwork $artwork) {
return (object) array_merge($this->mapArtworkCardPayload($artwork), [
'label' => $artwork->featured_label,
'featured_at' => $this->formatIsoDate($artwork->featured_slot_at),
]);
});
}
@@ -972,6 +997,10 @@ class ProfileController extends Controller
->where('a.is_public', true)
->where('a.is_approved', true)
->whereNotNull('a.published_at')
->when(app(ArtworkMaturityService::class)->viewerPreferences($viewer)['visibility'] === ArtworkMaturityService::VIEW_HIDE, function ($query): void {
$query->whereRaw('COALESCE(a.is_mature, 0) = 0')
->whereRaw("COALESCE(a.maturity_status, 'clear') != ?", [ArtworkMaturityService::STATUS_SUSPECTED]);
})
->orderByDesc('af.created_at')
->orderByDesc('af.artwork_id')
->limit($favouriteLimit + 1)
@@ -981,7 +1010,16 @@ class ProfileController extends Controller
$hasMore = $favIds->count() > $favouriteLimit;
$favIds = $favIds->take($favouriteLimit);
$indexed = Artwork::with('user:id,name,username')
$indexed = Artwork::with([
'user:id,name,username,level,rank',
'user.profile:user_id,avatar_hash',
'group:id,name,slug,avatar_path',
'stats:artwork_id,views,downloads,favorites',
'categories' => function ($query) {
$query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
->with(['contentType:id,slug,name']);
},
])
->whereIn('id', $favIds)
->get()
->keyBy('id');
@@ -1056,18 +1094,38 @@ class ProfileController extends Controller
->count();
}
$liveAwardsReceivedCount = 0;
if (Schema::hasTable('artwork_awards') && Schema::hasTable('artworks')) {
$liveAwardsReceivedCount = (int) DB::table('artwork_awards as aw')
->join('artworks as a', 'a.id', '=', 'aw.artwork_id')
$medalTotals = [
'gold' => 0,
'silver' => 0,
'bronze' => 0,
'count' => 0,
'score_total' => 0,
];
if (Schema::hasTable('artwork_medal_stats') && Schema::hasTable('artworks')) {
$totals = DB::table('artwork_medal_stats as aas')
->join('artworks as a', 'a.id', '=', 'aas.artwork_id')
->where('a.user_id', $user->id)
->whereNull('a.deleted_at')
->count();
->selectRaw('COALESCE(SUM(aas.gold_count), 0) as gold_count')
->selectRaw('COALESCE(SUM(aas.silver_count), 0) as silver_count')
->selectRaw('COALESCE(SUM(aas.bronze_count), 0) as bronze_count')
->selectRaw('COALESCE(SUM(aas.score_total), 0) as score_total')
->first();
$medalTotals = [
'gold' => (int) ($totals->gold_count ?? 0),
'silver' => (int) ($totals->silver_count ?? 0),
'bronze' => (int) ($totals->bronze_count ?? 0),
'count' => (int) (($totals->gold_count ?? 0) + ($totals->silver_count ?? 0) + ($totals->bronze_count ?? 0)),
'score_total' => (int) ($totals->score_total ?? 0),
];
}
$statsPayload = array_merge($stats ? (array) $stats : [], [
'uploads_count' => $liveUploadsCount,
'awards_received_count' => $liveAwardsReceivedCount,
'awards_received_count' => $medalTotals['count'],
'medal_totals' => $medalTotals,
'followers_count' => (int) $followerCount,
'following_count' => (int) $followingCount,
]);
@@ -1145,7 +1203,7 @@ class ProfileController extends Controller
]);
$profileCollections = $this->collections->getProfileCollections($user, $viewer);
$profileCollectionsPayload = $this->collections->mapCollectionCardPayloads($profileCollections, $isOwner);
$profileCollectionsPayload = $this->collections->mapCollectionCardPayloads($profileCollections, $isOwner, $viewer);
// ── Profile data ─────────────────────────────────────────────────────
$profile = $user->profile;
@@ -1203,6 +1261,7 @@ class ProfileController extends Controller
$achievementSummary = $this->achievements->summary((int) $user->id);
$leaderboardRank = $this->leaderboards->creatorRankSummary((int) $user->id);
$groupContributionHistory = $this->buildGroupContributionHistory($user);
$journey = $this->creatorJourney->publicPayloadForUser($user);
$resolvedInitialTab = $this->normalizeProfileTab($initialTab);
$isTabLanding = ! $galleryOnly && $resolvedInitialTab !== null;
$activeProfileUrl = $resolvedInitialTab !== null
@@ -1276,6 +1335,7 @@ class ProfileController extends Controller
'collections' => $profileCollectionsPayload,
'achievements' => $achievementSummary,
'leaderboardRank' => $leaderboardRank,
'journey' => $journey,
'groupContributionHistory' => $groupContributionHistory,
'countryName' => $countryName,
'isOwner' => $isOwner,
@@ -1288,6 +1348,7 @@ class ProfileController extends Controller
'collectionsFeaturedUrl' => route('collections.featured'),
'collectionFeatureLimit' => (int) config('collections.featured_limit', 3),
'profileTabUrls' => $profileTabUrls,
'journeyApiUrl' => route('api.profile.journey', ['username' => $usernameSlug]),
])->withViewData([
'page_title' => $pageTitle,
'page_canonical' => $galleryOnly ? $galleryUrl : $activeProfileUrl,
@@ -1435,8 +1496,17 @@ class ProfileController extends Controller
$category = $art->categories->first();
$contentType = $category?->contentType;
$stats = $art->stats;
$group = $art->group;
$isGroupPublisher = $group !== null;
$displayName = $isGroupPublisher ? ($group->name ?? 'Skinbase') : ($art->user?->name ?? 'Skinbase');
$username = $isGroupPublisher ? null : ($art->user?->username ?? null);
$avatarUrl = $isGroupPublisher ? $group->avatarUrl() : ($art->user?->profile?->avatar_url ?? null);
$profileUrl = $isGroupPublisher
? $group->publicUrl()
: ($username ? '/@' . $username : null);
$publisherType = $isGroupPublisher ? 'group' : 'user';
return [
return app(ArtworkMaturityService::class)->decoratePayload([
'id' => $art->id,
'name' => $art->title,
'picture' => $art->file_name,
@@ -1444,11 +1514,22 @@ class ProfileController extends Controller
'published_at' => $this->formatIsoDate($art->published_at),
'thumb' => $present['url'],
'thumb_srcset' => $present['srcset'] ?? $present['url'],
'uname' => $art->user->name ?? 'Skinbase',
'username' => $art->user->username ?? null,
'uname' => $displayName,
'username' => $username,
'avatar_url' => $avatarUrl,
'profile_url' => $profileUrl,
'published_as_type' => $publisherType,
'publisher' => [
'type' => $publisherType,
'id' => $isGroupPublisher ? (int) $group->id : (int) ($art->user?->id ?? 0),
'name' => $displayName,
'username' => $username ?? '',
'avatar_url' => $avatarUrl,
'profile_url' => $profileUrl,
],
'user_id' => $art->user_id,
'author_level' => (int) ($art->user?->level ?? 1),
'author_rank' => (string) ($art->user?->rank ?? 'Newbie'),
'author_level' => $isGroupPublisher ? 0 : (int) ($art->user?->level ?? 1),
'author_rank' => $isGroupPublisher ? '' : (string) ($art->user?->rank ?? 'Newbie'),
'content_type' => $contentType?->name,
'content_type_slug' => $contentType?->slug,
'category' => $category?->name,
@@ -1458,7 +1539,7 @@ class ProfileController extends Controller
'likes' => (int) ($stats?->favorites ?? $art->favourite_count ?? 0),
'width' => $art->width,
'height' => $art->height,
];
], $art, request()->user());
}
private function formatIsoDate(mixed $value): ?string