feat(homepage): Nova homepage layout — guest/auth split, mascot category tiles, 5-col artwork grids
- HomeController: is_logged_in now lives inside props JSON (not separate view var) - HomepageService: allForUser() adds user_data, fresh, suggested_creators; auth payload includes is_logged_in:true; guest payload merged with is_logged_in:false - getTrending/getFreshUploads/getFollowingFeed: limit 12→10 (2 clean rows of 5) - New getUserData() — unread messages + notifications counts via DB - New getSuggestedCreators() — top followed-count creators not yet followed, cached 5 min React — new components: - HomeWelcomeRow: greeting bar with avatar, unread message/notification badges, upload CTA - HomeFromFollowing: art grid from followed creators with empty-state - HomeTrendingForYou: personalized grid adapts heading/link to user's top tag - HomeBecauseYouLike: secondary personalized section keyed on top tag - HomeSuggestedCreators: 4-col creator cards with avatar, stats, View Profile link - HomeCTA: gradient upload banner; guest sees Create Account second CTA - HomeCategories: 5-tile static category grid with mascot images React — updated: - HomePage: split into GuestHomePage + AuthHomePage; routes on is_logged_in prop - HomeHero: isLoggedIn prop; upload href gates on auth; CTA → /discover/trending - HomeTrending: see-all → /discover/trending; grid 4→5 cols; slice to multiple of 5 - HomeFresh: see-all → /discover/fresh; grid 4→5 cols; slice to multiple of 5 - HomeFromFollowing/TrendingForYou/BecauseYouLike: 5-col grid, slice to multiple of 5 - HomeCategories: mascots per category (wallpapers/photography/skins/other), smaller tiles
This commit is contained in:
@@ -54,26 +54,32 @@ final class HomepageService
|
||||
* Personalized homepage data for an authenticated user.
|
||||
*
|
||||
* Sections:
|
||||
* 1. from_following – artworks from creators you follow
|
||||
* 2. trending – same trending feed as guests
|
||||
* 3. by_tags – artworks matching user's top tags
|
||||
* 4. by_categories – fresh uploads in user's favourite categories
|
||||
* 5. tags / creators / news – shared with guest homepage
|
||||
* 1. user_data – welcome row counts (messages, notifications, new followers)
|
||||
* 2. from_following – artworks from creators you follow
|
||||
* 3. trending – same trending feed as guests
|
||||
* 4. by_tags – artworks matching user's top tags (Trending For You)
|
||||
* 5. by_categories – fresh uploads in user's favourite categories
|
||||
* 6. suggested_creators – creators the user might want to follow
|
||||
* 7. tags / creators / news – shared with guest homepage
|
||||
*/
|
||||
public function allForUser(\App\Models\User $user): array
|
||||
{
|
||||
$prefs = $this->prefs->build($user);
|
||||
|
||||
return [
|
||||
'hero' => $this->getHeroArtwork(),
|
||||
'from_following' => $this->getFollowingFeed($user, $prefs),
|
||||
'trending' => $this->getTrending(),
|
||||
'by_tags' => $this->getByTags($prefs['top_tags'] ?? []),
|
||||
'by_categories' => $this->getByCategories($prefs['top_categories'] ?? []),
|
||||
'tags' => $this->getPopularTags(),
|
||||
'creators' => $this->getCreatorSpotlight(),
|
||||
'news' => $this->getNews(),
|
||||
'preferences' => [
|
||||
'is_logged_in' => true,
|
||||
'user_data' => $this->getUserData($user),
|
||||
'hero' => $this->getHeroArtwork(),
|
||||
'from_following' => $this->getFollowingFeed($user, $prefs),
|
||||
'trending' => $this->getTrending(),
|
||||
'fresh' => $this->getFreshUploads(),
|
||||
'by_tags' => $this->getByTags($prefs['top_tags'] ?? []),
|
||||
'by_categories' => $this->getByCategories($prefs['top_categories'] ?? []),
|
||||
'suggested_creators' => $this->getSuggestedCreators($user, $prefs),
|
||||
'tags' => $this->getPopularTags(),
|
||||
'creators' => $this->getCreatorSpotlight(),
|
||||
'news' => $this->getNews(),
|
||||
'preferences' => [
|
||||
'top_tags' => $prefs['top_tags'] ?? [],
|
||||
'top_categories' => $prefs['top_categories'] ?? [],
|
||||
],
|
||||
@@ -114,7 +120,7 @@ final class HomepageService
|
||||
* Falls back to DB ORDER BY trending_score_7d if Meilisearch is unavailable.
|
||||
* Spec: no heavy joins in the hot path.
|
||||
*/
|
||||
public function getTrending(int $limit = 12): array
|
||||
public function getTrending(int $limit = 10): array
|
||||
{
|
||||
return Cache::remember("homepage.trending.{$limit}", self::CACHE_TTL, function () use ($limit): array {
|
||||
try {
|
||||
@@ -166,7 +172,7 @@ final class HomepageService
|
||||
/**
|
||||
* Fresh uploads: latest 12 approved public artworks.
|
||||
*/
|
||||
public function getFreshUploads(int $limit = 12): array
|
||||
public function getFreshUploads(int $limit = 10): array
|
||||
{
|
||||
return Cache::remember("homepage.fresh.{$limit}", self::CACHE_TTL, function () use ($limit): array {
|
||||
$artworks = Artwork::public()
|
||||
@@ -315,6 +321,95 @@ final class HomepageService
|
||||
// Personalized sections (auth only)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Welcome-row counts: unread messages, unread notifications, new followers.
|
||||
* Returns quickly from DB using simple COUNTs; never throws.
|
||||
*/
|
||||
public function getUserData(\App\Models\User $user): array
|
||||
{
|
||||
try {
|
||||
$unreadMessages = DB::table('conversations as c')
|
||||
->join('conversation_participants as cp', 'cp.conversation_id', '=', 'c.id')
|
||||
->join('messages as m', 'm.conversation_id', '=', 'c.id')
|
||||
->where('cp.user_id', $user->id)
|
||||
->where('m.user_id', '!=', $user->id)
|
||||
->whereColumn('m.created_at', '>', 'cp.last_read_at')
|
||||
->distinct('c.id')
|
||||
->count('c.id');
|
||||
} catch (\Throwable) {
|
||||
$unreadMessages = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$unreadNotifications = DB::table('notifications')
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('read_at')
|
||||
->count();
|
||||
} catch (\Throwable) {
|
||||
$unreadNotifications = 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'username' => $user->username,
|
||||
'avatar' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash ?? null, 64),
|
||||
'messages_unread' => (int) $unreadMessages,
|
||||
'notifications_unread' => (int) $unreadNotifications,
|
||||
'followers_count' => (int) ($user->statistics?->followers_count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggested creators: active public uploaders NOT already followed by the user,
|
||||
* ranked by follower count. Optionally filtered to the user's top categories.
|
||||
*/
|
||||
public function getSuggestedCreators(\App\Models\User $user, array $prefs, int $limit = 8): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"homepage.suggested.{$user->id}",
|
||||
300,
|
||||
function () use ($user, $prefs, $limit): array {
|
||||
try {
|
||||
$followingIds = $prefs['followed_creators'] ?? [];
|
||||
|
||||
$query = DB::table('users as u')
|
||||
->join('user_profiles as up', 'up.user_id', '=', 'u.id')
|
||||
->leftJoin('user_statistics as us', 'us.user_id', '=', 'u.id')
|
||||
->select(
|
||||
'u.id',
|
||||
'u.name',
|
||||
'u.username',
|
||||
'up.avatar_hash',
|
||||
DB::raw('COALESCE(us.followers_count, 0) as followers_count'),
|
||||
DB::raw('COALESCE(us.artworks_count, 0) as artworks_count'),
|
||||
)
|
||||
->where('u.id', '!=', $user->id)
|
||||
->whereNotIn('u.id', array_merge($followingIds, [$user->id]))
|
||||
->where('u.is_active', true)
|
||||
->orderByDesc('followers_count')
|
||||
->orderByDesc('artworks_count')
|
||||
->limit($limit);
|
||||
|
||||
$rows = $query->get();
|
||||
|
||||
return $rows->map(fn ($u) => [
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'username' => $u->username,
|
||||
'url' => $u->username ? '/@' . $u->username : '/profile/' . $u->id,
|
||||
'avatar' => AvatarUrl::forUser((int) $u->id, $u->avatar_hash ?: null, 64),
|
||||
'followers_count' => (int) $u->followers_count,
|
||||
'artworks_count' => (int) $u->artworks_count,
|
||||
])->values()->all();
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('HomepageService::getSuggestedCreators failed', ['error' => $e->getMessage()]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest artworks from creators the user follows (max 12).
|
||||
*/
|
||||
@@ -335,7 +430,7 @@ final class HomepageService
|
||||
->with(['user:id,name,username', 'user.profile:user_id,avatar_hash'])
|
||||
->whereIn('user_id', $followingIds)
|
||||
->orderByDesc('published_at')
|
||||
->limit(12)
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
return $artworks->map(fn ($a) => $this->serializeArtwork($a))->values()->all();
|
||||
|
||||
Reference in New Issue
Block a user