From 4f9b43bbba99ee3cbe9c1f1d35a063ab9334f6e9 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Fri, 27 Feb 2026 10:48:35 +0100 Subject: [PATCH] =?UTF-8?q?feat(homepage):=20Nova=20homepage=20layout=20?= =?UTF-8?q?=E2=80=94=20guest/auth=20split,=20mascot=20category=20tiles,=20?= =?UTF-8?q?5-col=20artwork=20grids?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/Http/Controllers/Web/HomeController.php | 7 +- app/Services/HomepageService.php | 129 +++++++++++++--- .../js/Pages/Home/HomeBecauseYouLike.jsx | 74 +++++++++ resources/js/Pages/Home/HomeCTA.jsx | 44 ++++++ resources/js/Pages/Home/HomeCategories.jsx | 99 ++++++++++++ resources/js/Pages/Home/HomeFresh.jsx | 6 +- resources/js/Pages/Home/HomeFromFollowing.jsx | 83 +++++++++++ resources/js/Pages/Home/HomeHero.jsx | 18 ++- resources/js/Pages/Home/HomePage.jsx | 141 ++++++++++++++++-- .../js/Pages/Home/HomeSuggestedCreators.jsx | 67 +++++++++ resources/js/Pages/Home/HomeTrending.jsx | 6 +- .../js/Pages/Home/HomeTrendingForYou.jsx | 70 +++++++++ resources/js/Pages/Home/HomeWelcomeRow.jsx | 74 +++++++++ 13 files changed, 771 insertions(+), 47 deletions(-) create mode 100644 resources/js/Pages/Home/HomeBecauseYouLike.jsx create mode 100644 resources/js/Pages/Home/HomeCTA.jsx create mode 100644 resources/js/Pages/Home/HomeCategories.jsx create mode 100644 resources/js/Pages/Home/HomeFromFollowing.jsx create mode 100644 resources/js/Pages/Home/HomeSuggestedCreators.jsx create mode 100644 resources/js/Pages/Home/HomeTrendingForYou.jsx create mode 100644 resources/js/Pages/Home/HomeWelcomeRow.jsx diff --git a/app/Http/Controllers/Web/HomeController.php b/app/Http/Controllers/Web/HomeController.php index 173105b0..bbfce2e0 100644 --- a/app/Http/Controllers/Web/HomeController.php +++ b/app/Http/Controllers/Web/HomeController.php @@ -17,7 +17,7 @@ final class HomeController extends Controller $user = $request->user(); $sections = $user ? $this->homepage->allForUser($user) - : $this->homepage->all(); + : array_merge($this->homepage->all(), ['is_logged_in' => false]); $hero = $sections['hero']; @@ -30,9 +30,8 @@ final class HomeController extends Controller ]; return view('web.home', [ - 'meta' => $meta, - 'props' => $sections, - 'is_logged_in' => (bool) $user, + 'meta' => $meta, + 'props' => $sections, ]); } } diff --git a/app/Services/HomepageService.php b/app/Services/HomepageService.php index 2b5687c2..98c5a482 100644 --- a/app/Services/HomepageService.php +++ b/app/Services/HomepageService.php @@ -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(); diff --git a/resources/js/Pages/Home/HomeBecauseYouLike.jsx b/resources/js/Pages/Home/HomeBecauseYouLike.jsx new file mode 100644 index 00000000..50b93cd3 --- /dev/null +++ b/resources/js/Pages/Home/HomeBecauseYouLike.jsx @@ -0,0 +1,74 @@ +import React from 'react' + +const FALLBACK = 'https://files.skinbase.org/default/missing_md.webp' +const AVATAR_FALLBACK = 'https://files.skinbase.org/avatars/default.webp' + +function ArtCard({ item }) { + const username = item.author_username ? `@${item.author_username}` : null + return ( +
+ +
+
+ {item.title} { e.currentTarget.src = FALLBACK }} + /> +
+
{item.title}
+
+ {item.author} { e.currentTarget.src = AVATAR_FALLBACK }} + /> + {item.author} + {username && {username}} +
+
+
+ {item.title} by {item.author} +
+
+ ) +} + +/** + * Because You Like {tag}: fresh or trending artworks for the user's top tag. + * Only rendered when by_categories data is available and a top tag is known. + */ +export default function HomeBecauseYouLike({ items, preferences }) { + const topTag = preferences?.top_tags?.[0] + + if (!Array.isArray(items) || items.length === 0 || !topTag) return null + + return ( +
+
+

+ ✨ Because You Like{' '} + #{topTag} +

+ + See all → + +
+
+ {items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => ( + + ))} +
+
+ ) +} diff --git a/resources/js/Pages/Home/HomeCTA.jsx b/resources/js/Pages/Home/HomeCTA.jsx new file mode 100644 index 00000000..435d95dc --- /dev/null +++ b/resources/js/Pages/Home/HomeCTA.jsx @@ -0,0 +1,44 @@ +import React from 'react' + +/** + * Upload CTA banner — shown at the bottom of both guest and logged-in homepages. + */ +export default function HomeCTA({ isLoggedIn }) { + const uploadHref = isLoggedIn ? '/upload' : '/login?redirect=/upload' + + return ( +
+
+ {/* Decorative blobs */} +
+
+ +
+

Join the community

+

+ Ready to share your creativity? +

+

+ Upload your artworks, wallpapers, and skins to reach thousands of enthusiasts around the world. +

+
+ + Upload your artwork + + {!isLoggedIn && ( + + Create account + + )} +
+
+
+
+ ) +} diff --git a/resources/js/Pages/Home/HomeCategories.jsx b/resources/js/Pages/Home/HomeCategories.jsx new file mode 100644 index 00000000..bb0f59da --- /dev/null +++ b/resources/js/Pages/Home/HomeCategories.jsx @@ -0,0 +1,99 @@ +import React from 'react' + +const CATEGORIES = [ + { + label: 'Wallpapers', + description: 'Desktop & mobile backgrounds', + href: '/wallpapers', + icon: '🖥️', + mascot: '/gfx/mascot_wallpapers.webp', + color: 'from-sky-500/20 to-sky-900/40', + }, + { + label: 'Photography', + description: 'Real-world captures & edits', + href: '/photography', + icon: '📷', + mascot: '/gfx/mascot_photography.webp', + color: 'from-emerald-500/20 to-emerald-900/40', + }, + { + label: 'Skins', + description: 'App & game skins', + href: '/skins', + icon: '🎨', + mascot: '/gfx/mascot_skins.webp', + color: 'from-purple-500/20 to-purple-900/40', + }, + { + label: 'Digital Art', + description: 'Illustrations & concept art', + href: '/other', + icon: '✏️', + mascot: '/gfx/mascot_other.webp', + color: 'from-rose-500/20 to-rose-900/40', + }, + { + label: 'Tags Hub', + description: 'Browse by theme or style', + href: '/tags', + icon: '🏷️', + mascot: '/gfx/mascot_other.webp', + color: 'from-amber-500/20 to-amber-900/40', + }, +] + +function CategoryTile({ cat }) { + return ( + +
+ + {/* Mascot image — bottom-right, partially overflowing bottom edge */} + {cat.mascot && ( + + )} + + {/* Text label — bottom-left, always readable */} +
+ {!cat.mascot && ( + {cat.icon} + )} +

{cat.label}

+

{cat.description}

+
+
+ ) +} + +/** + * Static category quick-links. No backend data needed — these are fixed routes. + */ +export default function HomeCategories() { + return ( +
+
+

🗂️ Explore Categories

+ + Browse all → + +
+ + {/* 5 tiles: 2 rows on mobile, single row on xl */} +
+ {CATEGORIES.map((cat) => ( + + ))} +
+
+ ) +} diff --git a/resources/js/Pages/Home/HomeFresh.jsx b/resources/js/Pages/Home/HomeFresh.jsx index 8410a685..4a0577f0 100644 --- a/resources/js/Pages/Home/HomeFresh.jsx +++ b/resources/js/Pages/Home/HomeFresh.jsx @@ -58,13 +58,13 @@ export default function HomeFresh({ items }) {

🆕 Fresh Uploads

- + See all →
-
- {items.map((item) => ( +
+ {items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => ( ))}
diff --git a/resources/js/Pages/Home/HomeFromFollowing.jsx b/resources/js/Pages/Home/HomeFromFollowing.jsx new file mode 100644 index 00000000..0791d70d --- /dev/null +++ b/resources/js/Pages/Home/HomeFromFollowing.jsx @@ -0,0 +1,83 @@ +import React from 'react' + +const FALLBACK = 'https://files.skinbase.org/default/missing_md.webp' +const AVATAR_FALLBACK = 'https://files.skinbase.org/avatars/default.webp' + +function ArtCard({ item }) { + const username = item.author_username ? `@${item.author_username}` : null + return ( + + ) +} + +export default function HomeFromFollowing({ items }) { + // Empty state: user follows nobody + if (!Array.isArray(items) || items.length === 0) { + return ( +
+
+

👥 From Creators You Follow

+
+
+

You're not following anyone yet.

+

+ Follow creators you love to see their latest uploads here. +

+ + Discover creators → + +
+
+ ) + } + + return ( +
+
+

👥 From Creators You Follow

+ + See all → + +
+
+ {items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => ( + + ))} +
+
+ ) +} diff --git a/resources/js/Pages/Home/HomeHero.jsx b/resources/js/Pages/Home/HomeHero.jsx index f838ffa9..498b79e7 100644 --- a/resources/js/Pages/Home/HomeHero.jsx +++ b/resources/js/Pages/Home/HomeHero.jsx @@ -2,21 +2,23 @@ import React from 'react' const FALLBACK = 'https://files.skinbase.org/default/missing_lg.webp' -export default function HomeHero({ artwork }) { +export default function HomeHero({ artwork, isLoggedIn }) { + const uploadHref = isLoggedIn ? '/upload' : '/login?redirect=/upload' + if (!artwork) { return (

- Discover Digital Art + Skinbase Nova

- Wallpapers, skins & digital creations from a global community. + Discover. Create. Inspire.

@@ -53,13 +55,13 @@ export default function HomeHero({ artwork }) {

- Explore + Explore Trending Upload diff --git a/resources/js/Pages/Home/HomePage.jsx b/resources/js/Pages/Home/HomePage.jsx index 7c1d3c64..94432c6e 100644 --- a/resources/js/Pages/Home/HomePage.jsx +++ b/resources/js/Pages/Home/HomePage.jsx @@ -1,14 +1,22 @@ import React, { lazy, Suspense } from 'react' import { createRoot } from 'react-dom/client' -// Sub-section components — lazy-loaded so only the hero blocks the initial bundle +// Above-fold — eager import HomeHero from './HomeHero' -const HomeTrending = lazy(() => import('./HomeTrending')) -const HomeFresh = lazy(() => import('./HomeFresh')) -const HomeTags = lazy(() => import('./HomeTags')) -const HomeCreators = lazy(() => import('./HomeCreators')) -const HomeNews = lazy(() => import('./HomeNews')) +// Below-fold — lazy-loaded to keep initial bundle small +const HomeWelcomeRow = lazy(() => import('./HomeWelcomeRow')) +const HomeFromFollowing = lazy(() => import('./HomeFromFollowing')) +const HomeTrendingForYou = lazy(() => import('./HomeTrendingForYou')) +const HomeBecauseYouLike = lazy(() => import('./HomeBecauseYouLike')) +const HomeSuggestedCreators = lazy(() => import('./HomeSuggestedCreators')) +const HomeTrending = lazy(() => import('./HomeTrending')) +const HomeFresh = lazy(() => import('./HomeFresh')) +const HomeCategories = lazy(() => import('./HomeCategories')) +const HomeTags = lazy(() => import('./HomeTags')) +const HomeCreators = lazy(() => import('./HomeCreators')) +const HomeNews = lazy(() => import('./HomeNews')) +const HomeCTA = lazy(() => import('./HomeCTA')) function SectionFallback() { return ( @@ -16,32 +24,141 @@ function SectionFallback() { ) } -function HomePage({ hero, trending, fresh, tags, creators, news }) { - return ( -
- {/* Hero — above-fold, eager */} - +function GuestHomePage(props) { + const { hero, trending, fresh, tags, creators, news } = props - {/* Below-fold sections — lazy */} + return ( + <> + {/* 1. Hero */} + }> + {/* 3. Fresh Uploads */} }> + {/* 4. Explore Categories */} + }> + + + + {/* 5. Popular Tags */} }> + {/* 6. Top Creators */} }> + {/* 7. News */} }> + + {/* 8. CTA Upload */} + }> + + + + ) +} + +function AuthHomePage(props) { + const { + user_data, + hero, + from_following, + trending, + fresh, + by_tags, + by_categories, + suggested_creators, + tags, + creators, + news, + preferences, + } = props + + return ( + <> + {/* P0. Welcome/status row */} + + + + + {/* 1. Hero */} + + + {/* P2. From Creators You Follow */} + }> + + + + {/* P3. Trending For You (by_tags = Meilisearch tag overlap sorted by trending) */} + }> + + + + {/* 2. Global Trending Now */} + }> + + + + {/* P4. Because You Like {top tag} — uses by_categories for variety */} + }> + + + + {/* 3. Fresh Uploads */} + }> + + + + {/* 4. Explore Categories */} + }> + + + + {/* P5. Suggested Creators */} + }> + + + + {/* 5. Popular Tags */} + }> + + + + {/* 6. Top Creators */} + }> + + + + {/* 7. News */} + }> + + + + {/* 8. CTA Upload */} + }> + + + + ) +} + +function HomePage(props) { + return ( +
+ {props.is_logged_in + ? + : + }
) } diff --git a/resources/js/Pages/Home/HomeSuggestedCreators.jsx b/resources/js/Pages/Home/HomeSuggestedCreators.jsx new file mode 100644 index 00000000..3ee77c92 --- /dev/null +++ b/resources/js/Pages/Home/HomeSuggestedCreators.jsx @@ -0,0 +1,67 @@ +import React from 'react' + +const AVATAR_FALLBACK = 'https://files.skinbase.org/avatars/default.webp' + +function CreatorCard({ creator }) { + return ( +
+ ) +} + +export default function HomeSuggestedCreators({ creators }) { + if (!Array.isArray(creators) || creators.length === 0) return null + + return ( +
+
+
+

💡 Suggested Creators

+

Creators you might enjoy following

+
+ + Explore all → + +
+ +
+ {creators.map((creator) => ( + + ))} +
+
+ ) +} diff --git a/resources/js/Pages/Home/HomeTrending.jsx b/resources/js/Pages/Home/HomeTrending.jsx index 90ca2d89..b1042b21 100644 --- a/resources/js/Pages/Home/HomeTrending.jsx +++ b/resources/js/Pages/Home/HomeTrending.jsx @@ -60,13 +60,13 @@ export default function HomeTrending({ items }) {

🔥 Trending This Week

- + See all →
-
- {items.map((item) => ( +
+ {items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => ( ))}
diff --git a/resources/js/Pages/Home/HomeTrendingForYou.jsx b/resources/js/Pages/Home/HomeTrendingForYou.jsx new file mode 100644 index 00000000..f072d7ed --- /dev/null +++ b/resources/js/Pages/Home/HomeTrendingForYou.jsx @@ -0,0 +1,70 @@ +import React from 'react' + +const FALLBACK = 'https://files.skinbase.org/default/missing_md.webp' +const AVATAR_FALLBACK = 'https://files.skinbase.org/avatars/default.webp' + +function ArtCard({ item }) { + const username = item.author_username ? `@${item.author_username}` : null + return ( + + ) +} + +/** + * Personalized trending: artworks matching user's top tags, sorted by trending score. + * Label and browse link adapt to the user's first top tag. + */ +export default function HomeTrendingForYou({ items, preferences }) { + if (!Array.isArray(items) || items.length === 0) return null + + const topTag = preferences?.top_tags?.[0] + const heading = topTag ? `🎯 Trending in #${topTag}` : '🎯 Trending For You' + const link = topTag ? `/browse?tags=${encodeURIComponent(topTag)}&sort=trending` : '/discover/trending' + + return ( +
+
+

{heading}

+ + See all → + +
+
+ {items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => ( + + ))} +
+
+ ) +} diff --git a/resources/js/Pages/Home/HomeWelcomeRow.jsx b/resources/js/Pages/Home/HomeWelcomeRow.jsx new file mode 100644 index 00000000..adc9cc18 --- /dev/null +++ b/resources/js/Pages/Home/HomeWelcomeRow.jsx @@ -0,0 +1,74 @@ +import React from 'react' + +const AVATAR_FALLBACK = 'https://files.skinbase.org/avatars/default.webp' + +export default function HomeWelcomeRow({ user_data }) { + if (!user_data) return null + + const { name, avatar, messages_unread, notifications_unread, url } = user_data + + const firstName = name?.split(' ')[0] || name || 'there' + + return ( +
+
+
+ + {/* Left: greeting */} +
+ + {name} { e.currentTarget.src = AVATAR_FALLBACK }} + /> + +
+

Welcome back,

+

{firstName}

+
+
+ + {/* Right: action badges */} +
+ + {messages_unread > 0 && ( + + + + + {messages_unread} new + + )} + + {notifications_unread > 0 && ( + + + + + {notifications_unread} + + )} + + + + + + Upload + +
+
+
+
+ ) +}