1244 lines
53 KiB
PHP
1244 lines
53 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Web;
|
|
|
|
use App\Events\NovaCards\NovaCardViewed;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Jobs\UpdateNovaCardStatsJob;
|
|
use App\Models\NovaCard;
|
|
use App\Models\NovaCardCategory;
|
|
use App\Models\NovaCardChallenge;
|
|
use App\Models\NovaCardChallengeEntry;
|
|
use App\Models\NovaCardCollection;
|
|
use App\Models\NovaCardCreatorPreset;
|
|
use App\Models\NovaCardTag;
|
|
use App\Models\NovaCardTemplate;
|
|
use App\Models\User;
|
|
use App\Services\NovaCards\NovaCardPresenter;
|
|
use App\Services\NovaCards\NovaCardCommentService;
|
|
use App\Services\NovaCards\NovaCardLineageService;
|
|
use App\Services\NovaCards\NovaCardRelatedCardsService;
|
|
use App\Services\NovaCards\NovaCardRisingService;
|
|
use App\Support\UsernamePolicy;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\View\View;
|
|
|
|
class NovaCardsController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly NovaCardPresenter $presenter,
|
|
private readonly NovaCardCommentService $comments,
|
|
private readonly NovaCardLineageService $lineage,
|
|
private readonly NovaCardRisingService $risingService,
|
|
private readonly NovaCardRelatedCardsService $relatedService,
|
|
) {}
|
|
|
|
public function index(Request $request): View
|
|
{
|
|
$latest = $this->publicCardsQuery()->latest('published_at')->paginate(18)->withQueryString();
|
|
$featured = $this->publicCardsQuery()->where('featured', true)->latest('published_at')->limit(6)->get();
|
|
$trending = $this->publicCardsQuery()->orderByDesc('trending_score')->orderByDesc('last_engaged_at')->limit(6)->get();
|
|
$rising = $this->risingService->risingCards(6);
|
|
$collections = NovaCardCollection::query()
|
|
->with(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags'])
|
|
->where(function ($query): void {
|
|
$query->where('official', true)
|
|
->orWhere('visibility', NovaCardCollection::VISIBILITY_PUBLIC);
|
|
})
|
|
->orderByDesc('featured')
|
|
->orderByDesc('official')
|
|
->orderByDesc('updated_at')
|
|
->limit(4)
|
|
->get();
|
|
$categories = NovaCardCategory::query()->where('active', true)->orderBy('order_num')->orderBy('name')->get();
|
|
$tags = NovaCardTag::query()->withCount('cards')->orderByDesc('cards_count')->limit(12)->get();
|
|
$styleFamilies = $this->styleFamilies();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => 'Nova Cards - Skinbase Nova',
|
|
'description' => 'Browse featured, trending, and latest Nova Cards. Discover beautiful quote cards, mood cards, and visual text art by the Skinbase Nova community.',
|
|
'canonical' => route('cards.index'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Nova Cards',
|
|
'subheading' => (string) config('nova_cards.brand.subtitle'),
|
|
'cards' => $this->presenter->cards($latest->items()),
|
|
'pagination' => $latest,
|
|
'featuredCards' => $this->presenter->cards($featured),
|
|
'trendingCards' => $this->presenter->cards($trending),
|
|
'risingCards' => $this->presenter->cards($rising->all()),
|
|
'categories' => $categories,
|
|
'tags' => $tags,
|
|
'moodFamilies' => $this->moodFamilies(),
|
|
'styleFamilies' => $styleFamilies,
|
|
'paletteFamilies' => $this->paletteFamilies(),
|
|
'seasonalHubs' => $this->seasonalHubs(),
|
|
'collections' => $collections->map(fn (NovaCardCollection $collection): array => $this->presenter->collection($collection, $request->user()))->values()->all(),
|
|
'context' => 'index',
|
|
]);
|
|
}
|
|
|
|
public function category(Request $request, string $categorySlug): View
|
|
{
|
|
$category = NovaCardCategory::query()->where('slug', $categorySlug)->where('active', true)->firstOrFail();
|
|
$cards = $this->publicCardsQuery()->where('category_id', $category->id)->latest('published_at')->paginate(18)->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => $category->name . ' Cards - Skinbase Nova',
|
|
'description' => $category->description ?: ('Browse ' . strtolower($category->name) . ' Nova Cards on Skinbase Nova.'),
|
|
'canonical' => route('cards.category', ['categorySlug' => $category->slug]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => $category->name,
|
|
'subheading' => $category->description ?: 'Explore this Nova Cards category.',
|
|
'cards' => $this->presenter->cards($cards->items()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect([$category]),
|
|
'tags' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'context' => 'category',
|
|
]);
|
|
}
|
|
|
|
public function popular(Request $request): View
|
|
{
|
|
$cards = $this->publicCardsQuery()
|
|
->orderByDesc('trending_score')
|
|
->orderByDesc('likes_count')
|
|
->orderByDesc('saves_count')
|
|
->paginate(18)
|
|
->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => 'Popular Cards - Skinbase Nova',
|
|
'description' => 'Browse the most liked, saved, and viewed Nova Cards on Skinbase Nova.',
|
|
'canonical' => route('cards.popular'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Popular cards',
|
|
'subheading' => 'The cards earning the strongest saves, likes, and repeat views right now.',
|
|
'cards' => $this->presenter->cards($cards->items(), false, $request->user()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'risingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'context' => 'popular',
|
|
]);
|
|
}
|
|
|
|
public function rising(Request $request): View
|
|
{
|
|
$risingCollection = $this->risingService->risingCards(36);
|
|
$page = max(1, (int) $request->query('page', 1));
|
|
$perPage = 18;
|
|
$paginated = new \Illuminate\Pagination\LengthAwarePaginator(
|
|
$risingCollection->forPage($page, $perPage)->values(),
|
|
$risingCollection->count(),
|
|
$perPage,
|
|
$page,
|
|
['path' => route('cards.rising')],
|
|
);
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => 'Rising Cards - Skinbase Nova',
|
|
'description' => 'Discover Nova Cards that are gaining traction right now — fresh creators and fast-rising saves and remixes.',
|
|
'canonical' => route('cards.rising'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Rising',
|
|
'subheading' => 'Fresh Nova Cards gaining momentum right now.',
|
|
'cards' => $this->presenter->cards($paginated->items(), false, $request->user()),
|
|
'pagination' => $paginated,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'risingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'context' => 'rising',
|
|
]);
|
|
}
|
|
|
|
public function remixed(Request $request): View
|
|
{
|
|
$cards = $this->publicCardsQuery()
|
|
->whereNotNull('original_card_id')
|
|
->orderByDesc('published_at')
|
|
->paginate(18)
|
|
->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => 'Remixed Cards - Skinbase Nova',
|
|
'description' => 'Discover Nova Cards remixed from community originals with attribution and lineage.',
|
|
'canonical' => route('cards.remixed'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Remixed cards',
|
|
'subheading' => 'Community reinterpretations linked back to their original Nova Cards.',
|
|
'cards' => $this->presenter->cards($cards->items(), false, $request->user()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'paletteFamilies' => collect(),
|
|
'context' => 'remixed',
|
|
]);
|
|
}
|
|
|
|
public function remixHighlights(Request $request): View
|
|
{
|
|
$cards = $this->publicCardsQuery()
|
|
->whereNotNull('original_card_id')
|
|
->orderByDesc('remixes_count')
|
|
->orderByDesc('saves_count')
|
|
->orderByDesc('likes_count')
|
|
->orderByDesc('published_at')
|
|
->paginate(18)
|
|
->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => 'Best Remixes - Skinbase Nova',
|
|
'description' => 'Browse standout Nova Card remixes ranked by remix traction, saves, and likes.',
|
|
'canonical' => route('cards.remix-highlights'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Best remixes',
|
|
'subheading' => 'The strongest community reinterpretations ranked by remix traction and engagement.',
|
|
'cards' => $this->presenter->cards($cards->items(), false, $request->user()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'paletteFamilies' => collect(),
|
|
'context' => 'remix-highlights',
|
|
]);
|
|
}
|
|
|
|
public function editorial(Request $request): View
|
|
{
|
|
$cards = NovaCard::query()
|
|
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags', 'originalCard', 'rootCard'])
|
|
->featuredEditorial()
|
|
->orderByDesc('featured_score')
|
|
->orderByDesc('featured')
|
|
->orderByDesc('published_at')
|
|
->paginate(18)
|
|
->withQueryString();
|
|
|
|
$collections = NovaCardCollection::query()
|
|
->with(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags'])
|
|
->where(function ($query): void {
|
|
$query->where('featured', true)
|
|
->orWhere('official', true);
|
|
})
|
|
->orderByDesc('featured')
|
|
->orderByDesc('official')
|
|
->orderByDesc('updated_at')
|
|
->limit(4)
|
|
->get();
|
|
|
|
$challenges = NovaCardChallenge::query()
|
|
->whereIn('status', [NovaCardChallenge::STATUS_ACTIVE, NovaCardChallenge::STATUS_COMPLETED])
|
|
->where(function ($query): void {
|
|
$query->where('featured', true)
|
|
->orWhere('official', true);
|
|
})
|
|
->orderByDesc('featured')
|
|
->orderBy('starts_at')
|
|
->limit(4)
|
|
->get();
|
|
$featuredCreators = User::query()
|
|
->where('nova_featured_creator', true)
|
|
->whereHas('novaCards', fn ($query) => $query->publiclyVisible())
|
|
->withCount([
|
|
'novaCards as public_cards_count' => fn ($query) => $query->publiclyVisible(),
|
|
'novaCards as featured_cards_count' => fn ($query) => $query->publiclyVisible()->where('featured', true),
|
|
])
|
|
->withSum([
|
|
'novaCards as total_views_count' => fn ($query) => $query->publiclyVisible(),
|
|
], 'views_count')
|
|
->orderByDesc('featured_cards_count')
|
|
->orderByDesc('public_cards_count')
|
|
->orderBy('username')
|
|
->limit(6)
|
|
->get()
|
|
->map(fn (User $creator): array => [
|
|
'id' => (int) $creator->id,
|
|
'username' => (string) $creator->username,
|
|
'display_name' => $creator->name ?: '@' . $creator->username,
|
|
'public_url' => route('cards.creator', ['username' => $creator->username]),
|
|
'public_cards_count' => (int) ($creator->public_cards_count ?? 0),
|
|
'featured_cards_count' => (int) ($creator->featured_cards_count ?? 0),
|
|
'total_views_count' => (int) ($creator->total_views_count ?? 0),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => 'Editorial Picks - Nova Cards - Skinbase Nova',
|
|
'description' => 'Browse editorial Nova Cards picks, featured collections, and highlighted challenges.',
|
|
'canonical' => route('cards.editorial'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Editorial picks',
|
|
'subheading' => 'Curated Nova Cards, featured collections, and standout challenge surfaces chosen for quality and cohesion.',
|
|
'cards' => $this->presenter->cards($cards->items(), false, $request->user()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'moodFamilies' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'paletteFamilies' => collect(),
|
|
'seasonalHubs' => collect(),
|
|
'featuredCreators' => $featuredCreators,
|
|
'landingCollections' => $collections->map(fn (NovaCardCollection $collection): array => $this->presenter->collection($collection, $request->user()))->values()->all(),
|
|
'landingChallenges' => $challenges,
|
|
'context' => 'editorial',
|
|
]);
|
|
}
|
|
|
|
public function seasonal(Request $request): View
|
|
{
|
|
$seasonalHubs = $this->seasonalHubs();
|
|
$cards = $this->cardsTaggedWithSlugs($seasonalHubs->pluck('tag_slugs')->flatten()->unique()->values()->all())
|
|
->latest('published_at')
|
|
->paginate(18)
|
|
->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => 'Seasonal Cards - Nova Cards - Skinbase Nova',
|
|
'description' => 'Browse seasonal and event-aware Nova Cards grouped by recurring moods, holidays, and time-of-year themes.',
|
|
'canonical' => route('cards.seasonal'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Seasonal cards',
|
|
'subheading' => 'Discover Nova Cards grouped by recurring seasonal and campaign-style themes.',
|
|
'cards' => $this->presenter->cards($cards->items(), false, $request->user()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'moodFamilies' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'paletteFamilies' => collect(),
|
|
'seasonalHubs' => $seasonalHubs,
|
|
'landingCollections' => [],
|
|
'landingChallenges' => collect(),
|
|
'context' => 'seasonal',
|
|
]);
|
|
}
|
|
|
|
public function challenges(Request $request): View
|
|
{
|
|
$challenges = NovaCardChallenge::query()
|
|
->with(['winnerCard.user'])
|
|
->whereIn('status', [NovaCardChallenge::STATUS_ACTIVE, NovaCardChallenge::STATUS_COMPLETED])
|
|
->orderByDesc('featured')
|
|
->orderBy('starts_at')
|
|
->get();
|
|
|
|
return view('cards.challenges', [
|
|
'meta' => [
|
|
'title' => 'Card Challenges - Skinbase Nova',
|
|
'description' => 'Browse active and completed Nova Cards challenges, prompts, and winners.',
|
|
'canonical' => route('cards.challenges'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Card challenges',
|
|
'subheading' => 'Official prompts and community challenge runs for Nova Cards creators.',
|
|
'challenges' => $challenges,
|
|
]);
|
|
}
|
|
|
|
public function challenge(Request $request, string $slug): View
|
|
{
|
|
$challenge = NovaCardChallenge::query()
|
|
->with(['winnerCard.user', 'entries.card.user'])
|
|
->where('slug', $slug)
|
|
->firstOrFail();
|
|
|
|
$entries = $challenge->entries
|
|
->filter(fn ($entry): bool => $entry->card !== null && $entry->card->canBeViewedBy($request->user()))
|
|
->sortByDesc(fn ($entry) => $entry->created_at)
|
|
->values();
|
|
|
|
return view('cards.challenges', [
|
|
'meta' => [
|
|
'title' => $challenge->title . ' - Skinbase Nova',
|
|
'description' => $challenge->description ?: 'Browse entries for this Nova Cards challenge.',
|
|
'canonical' => route('cards.challenges.show', ['slug' => $challenge->slug]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => $challenge->title,
|
|
'subheading' => $challenge->description ?: 'Challenge entries and winner picks.',
|
|
'challenge' => $challenge,
|
|
'challengeEntryItems' => $entries->map(fn ($entry): array => [
|
|
'id' => (int) $entry->id,
|
|
'status' => (string) $entry->status,
|
|
'note' => $entry->note,
|
|
'card' => $this->presenter->card($entry->card, false, $request->user()),
|
|
])->values()->all(),
|
|
'challenges' => collect([$challenge]),
|
|
]);
|
|
}
|
|
|
|
public function templates(Request $request): View
|
|
{
|
|
return view('cards.resources', [
|
|
'meta' => [
|
|
'title' => 'Template Packs - Skinbase Nova',
|
|
'description' => 'Browse official Nova Cards template packs and starting points.',
|
|
'canonical' => route('cards.templates'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Template packs',
|
|
'subheading' => 'Official starter packs for editorial, story, and community remix workflows.',
|
|
'packs' => collect($this->presenter->options()['template_packs'] ?? []),
|
|
'templates' => collect($this->presenter->options()['templates'] ?? []),
|
|
'resourceType' => 'template',
|
|
]);
|
|
}
|
|
|
|
public function assets(Request $request): View
|
|
{
|
|
return view('cards.resources', [
|
|
'meta' => [
|
|
'title' => 'Asset Packs - Skinbase Nova',
|
|
'description' => 'Browse official Nova Cards asset packs for decorative and editorial layouts.',
|
|
'canonical' => route('cards.assets'),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => 'Asset packs',
|
|
'subheading' => 'Official decorative and editorial pack sets for the Nova Cards v2 editor.',
|
|
'packs' => collect($this->presenter->options()['asset_packs'] ?? []),
|
|
'templates' => collect(),
|
|
'resourceType' => 'asset',
|
|
]);
|
|
}
|
|
|
|
public function tag(Request $request, string $tagSlug): View
|
|
{
|
|
$tag = NovaCardTag::query()->where('slug', $tagSlug)->firstOrFail();
|
|
$cards = $this->publicCardsQuery()->whereHas('tags', fn ($query) => $query->where('nova_card_tags.id', $tag->id))->latest('published_at')->paginate(18)->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => '#' . $tag->name . ' Cards - Skinbase Nova',
|
|
'description' => 'Browse Nova Cards tagged with #' . $tag->name . ' on Skinbase Nova.',
|
|
'canonical' => route('cards.tag', ['tagSlug' => $tag->slug]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => '#' . $tag->name,
|
|
'subheading' => 'Browse cards using this tag.',
|
|
'cards' => $this->presenter->cards($cards->items()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect([$tag]),
|
|
'moodFamilies' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'paletteFamilies' => collect(),
|
|
'seasonalHubs' => collect(),
|
|
'context' => 'tag',
|
|
]);
|
|
}
|
|
|
|
public function mood(Request $request, string $moodSlug): View
|
|
{
|
|
$mood = $this->moodFamilies()->firstWhere('key', $moodSlug);
|
|
abort_unless($mood !== null, 404);
|
|
|
|
$cards = $this->cardsTaggedWithSlugs((array) ($mood['tag_slugs'] ?? []))
|
|
->latest('published_at')
|
|
->paginate(18)
|
|
->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => $mood['label'] . ' Mood Cards - Skinbase Nova',
|
|
'description' => 'Browse Nova Cards grouped into the ' . strtolower((string) $mood['label']) . ' mood family on Skinbase Nova.',
|
|
'canonical' => route('cards.mood', ['moodSlug' => $mood['key']]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => $mood['label'],
|
|
'subheading' => 'Discover Nova Cards grouped by a curated mood family using durable tag mappings.',
|
|
'cards' => $this->presenter->cards($cards->items(), false, $request->user()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'moodFamilies' => $this->moodFamilies(),
|
|
'styleFamilies' => collect(),
|
|
'paletteFamilies' => collect(),
|
|
'seasonalHubs' => collect(),
|
|
'context' => 'mood',
|
|
]);
|
|
}
|
|
|
|
public function style(Request $request, string $styleSlug): View
|
|
{
|
|
$style = $this->styleFamilies()->firstWhere('key', $styleSlug);
|
|
abort_unless($style !== null, 404);
|
|
|
|
$cards = $this->publicCardsQuery()
|
|
->where('style_family', $style['key'])
|
|
->latest('published_at')
|
|
->paginate(18)
|
|
->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => $style['label'] . ' Style Cards - Skinbase Nova',
|
|
'description' => 'Browse Nova Cards using the ' . strtolower((string) $style['label']) . ' style family on Skinbase Nova.',
|
|
'canonical' => route('cards.style', ['styleSlug' => $style['key']]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => $style['label'],
|
|
'subheading' => 'Discover Nova Cards grouped by a shared visual style family.',
|
|
'cards' => $this->presenter->cards($cards->items(), false, $request->user()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'moodFamilies' => collect(),
|
|
'styleFamilies' => $this->styleFamilies(),
|
|
'paletteFamilies' => $this->paletteFamilies(),
|
|
'seasonalHubs' => collect(),
|
|
'context' => 'style',
|
|
]);
|
|
}
|
|
|
|
public function palette(Request $request, string $paletteSlug): View
|
|
{
|
|
$palette = $this->paletteFamilies()->firstWhere('key', $paletteSlug);
|
|
abort_unless($palette !== null, 404);
|
|
|
|
$cards = $this->publicCardsQuery()
|
|
->where('palette_family', $palette['key'])
|
|
->latest('published_at')
|
|
->paginate(18)
|
|
->withQueryString();
|
|
|
|
return view('cards.index', [
|
|
'meta' => [
|
|
'title' => $palette['label'] . ' Palette Cards - Skinbase Nova',
|
|
'description' => 'Browse Nova Cards using the ' . strtolower((string) $palette['label']) . ' palette family on Skinbase Nova.',
|
|
'canonical' => route('cards.palette', ['paletteSlug' => $palette['key']]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => $palette['label'],
|
|
'subheading' => 'Discover Nova Cards grouped by shared palette families and color direction.',
|
|
'cards' => $this->presenter->cards($cards->items(), false, $request->user()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'moodFamilies' => collect(),
|
|
'styleFamilies' => $this->styleFamilies(),
|
|
'paletteFamilies' => $this->paletteFamilies(),
|
|
'seasonalHubs' => collect(),
|
|
'context' => 'palette',
|
|
]);
|
|
}
|
|
|
|
public function creator(Request $request, string $username): View|RedirectResponse
|
|
{
|
|
$normalized = UsernamePolicy::normalize($username);
|
|
$user = User::query()->whereRaw('LOWER(username) = ?', [$normalized])->firstOrFail();
|
|
|
|
if ($username !== strtolower((string) $user->username)) {
|
|
return redirect()->route('cards.creator', ['username' => strtolower((string) $user->username)], 301);
|
|
}
|
|
|
|
return view('cards.index', array_merge($this->creatorPagePayload($request, $user), [
|
|
'meta' => [
|
|
'title' => '@' . $user->username . ' Cards - Skinbase Nova',
|
|
'description' => 'Browse Nova Cards created by @' . $user->username . ' on Skinbase Nova.',
|
|
'canonical' => route('cards.creator', ['username' => strtolower((string) $user->username)]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => '@' . $user->username,
|
|
'subheading' => 'Cards created by ' . ($user->name ?: '@' . $user->username),
|
|
'context' => 'creator',
|
|
]));
|
|
}
|
|
|
|
public function creatorPortfolio(Request $request, string $username): View|RedirectResponse
|
|
{
|
|
$normalized = UsernamePolicy::normalize($username);
|
|
$user = User::query()->whereRaw('LOWER(username) = ?', [$normalized])->firstOrFail();
|
|
|
|
if ($username !== strtolower((string) $user->username)) {
|
|
return redirect()->route('cards.creator.portfolio', ['username' => strtolower((string) $user->username)], 301);
|
|
}
|
|
|
|
return view('cards.index', array_merge($this->creatorPagePayload($request, $user), [
|
|
'meta' => [
|
|
'title' => '@' . $user->username . ' Portfolio - Skinbase Nova',
|
|
'description' => 'Browse the dedicated Nova Cards portfolio page for @' . $user->username . ' on Skinbase Nova.',
|
|
'canonical' => route('cards.creator.portfolio', ['username' => strtolower((string) $user->username)]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'heading' => '@' . $user->username . ' Portfolio',
|
|
'subheading' => 'A dedicated Nova Cards portfolio view for ' . ($user->name ?: '@' . $user->username) . '.',
|
|
'context' => 'creator-portfolio',
|
|
]));
|
|
}
|
|
|
|
private function creatorPagePayload(Request $request, User $user): array
|
|
{
|
|
|
|
$creatorCards = $this->publicCardsQuery()->where('user_id', $user->id);
|
|
$cards = (clone $creatorCards)->latest('published_at')->paginate(18)->withQueryString();
|
|
$creatorSummary = $this->buildCreatorSummary($user);
|
|
$creatorFeaturedWorks = $this->presenter->cards($this->creatorFeaturedWorks($user)->all(), false, $request->user());
|
|
$creatorFeaturedCollections = NovaCardCollection::query()
|
|
->with(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags'])
|
|
->where('user_id', $user->id)
|
|
->where('featured', true)
|
|
->where(function ($query): void {
|
|
$query->where('official', true)
|
|
->orWhere('visibility', NovaCardCollection::VISIBILITY_PUBLIC);
|
|
})
|
|
->orderByDesc('official')
|
|
->orderByDesc('updated_at')
|
|
->limit(3)
|
|
->get()
|
|
->map(fn (NovaCardCollection $collection): array => $this->presenter->collection($collection, $request->user()))
|
|
->values()
|
|
->all();
|
|
$creatorHighlights = $this->presenter->cards(
|
|
$this->creatorHighlights($user, $creatorFeaturedWorks !== [])->all(),
|
|
false,
|
|
$request->user()
|
|
);
|
|
$creatorMostRemixedWorks = $this->presenter->cards(
|
|
$this->creatorMostRemixedWorks($user)->all(),
|
|
false,
|
|
$request->user()
|
|
);
|
|
$creatorMostLikedWorks = $this->presenter->cards(
|
|
$this->creatorMostLikedWorks($user)->all(),
|
|
false,
|
|
$request->user()
|
|
);
|
|
$creatorRemixActivity = $this->creatorRemixActivity($user, $request->user());
|
|
$creatorRemixGraph = $this->creatorRemixGraph($user);
|
|
$creatorPreferenceSignals = $this->creatorPreferenceSignals($user);
|
|
$creatorTimeline = $this->creatorTimeline($user, $request->user());
|
|
$creatorChallengeHistory = $this->creatorChallengeHistory($user);
|
|
|
|
return [
|
|
'cards' => $this->presenter->cards($cards->items()),
|
|
'pagination' => $cards,
|
|
'featuredCards' => [],
|
|
'trendingCards' => [],
|
|
'categories' => collect(),
|
|
'tags' => collect(),
|
|
'moodFamilies' => collect(),
|
|
'styleFamilies' => collect(),
|
|
'paletteFamilies' => collect(),
|
|
'seasonalHubs' => collect(),
|
|
'creatorSummary' => $creatorSummary,
|
|
'creatorFeaturedWorks' => $creatorFeaturedWorks,
|
|
'creatorFeaturedCollections' => $creatorFeaturedCollections,
|
|
'creatorHighlights' => $creatorHighlights,
|
|
'creatorMostRemixedWorks' => $creatorMostRemixedWorks,
|
|
'creatorMostLikedWorks' => $creatorMostLikedWorks,
|
|
'creatorRemixActivity' => $creatorRemixActivity,
|
|
'creatorRemixGraph' => $creatorRemixGraph,
|
|
'creatorPreferenceSignals' => $creatorPreferenceSignals,
|
|
'creatorTimeline' => $creatorTimeline,
|
|
'creatorChallengeHistory' => $creatorChallengeHistory,
|
|
];
|
|
}
|
|
|
|
public function collection(Request $request, string $slug, int $id): View|RedirectResponse
|
|
{
|
|
$collection = NovaCardCollection::query()
|
|
->with(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags'])
|
|
->findOrFail($id);
|
|
|
|
abort_unless($collection->isPubliclyVisible(), 404);
|
|
|
|
if ($slug !== $collection->slug) {
|
|
return redirect()->route('cards.collections.show', ['slug' => $collection->slug, 'id' => $collection->id], 301);
|
|
}
|
|
|
|
return view('cards.collection', [
|
|
'meta' => [
|
|
'title' => $collection->name . ' - Nova Cards Collection - Skinbase Nova',
|
|
'description' => $collection->description ?: 'Browse this curated Nova Cards collection.',
|
|
'canonical' => route('cards.collections.show', ['slug' => $collection->slug, 'id' => $collection->id]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'collection' => $this->presenter->collection($collection, $request->user(), true),
|
|
]);
|
|
}
|
|
|
|
public function lineage(Request $request, string $slug, int $id): View|RedirectResponse
|
|
{
|
|
$card = NovaCard::query()
|
|
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags', 'originalCard.user', 'rootCard.user'])
|
|
->published()
|
|
->findOrFail($id);
|
|
|
|
abort_unless($card->canBeViewedBy($request->user()), 404);
|
|
|
|
if ($slug !== $card->slug) {
|
|
return redirect()->route('cards.lineage', ['slug' => $card->slug, 'id' => $card->id], 301);
|
|
}
|
|
|
|
$lineage = $this->lineage->resolve($card, $request->user());
|
|
|
|
return view('cards.lineage', [
|
|
'meta' => [
|
|
'title' => $card->title . ' Lineage - Nova Cards - Skinbase Nova',
|
|
'description' => 'Browse the remix lineage and related variants for this Nova Card.',
|
|
'canonical' => route('cards.lineage', ['slug' => $card->slug, 'id' => $card->id]),
|
|
'robots' => 'index,follow',
|
|
],
|
|
'card' => $lineage['card'],
|
|
'trail' => $lineage['trail'],
|
|
'rootCard' => $lineage['root_card'],
|
|
'familyCards' => $lineage['family_cards'],
|
|
]);
|
|
}
|
|
|
|
public function show(Request $request, string $slug, int $id): View|RedirectResponse
|
|
{
|
|
$card = NovaCard::query()
|
|
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags'])
|
|
->published()
|
|
->findOrFail($id);
|
|
|
|
if (! $card->canBeViewedBy($request->user())) {
|
|
abort(404);
|
|
}
|
|
|
|
if ($slug !== $card->slug) {
|
|
return redirect()->route('cards.show', ['slug' => $card->slug, 'id' => $card->id], 301);
|
|
}
|
|
|
|
$card->increment('views_count');
|
|
$card->refresh();
|
|
UpdateNovaCardStatsJob::dispatch($card->id);
|
|
event(new NovaCardViewed($card, $request->user()?->id));
|
|
|
|
$relatedByCategory = $card->category_id
|
|
? $this->publicCardsQuery()->where('category_id', $card->category_id)->where('id', '!=', $card->id)->limit(6)->get()
|
|
: collect();
|
|
$relatedByTags = $card->tags->isNotEmpty()
|
|
? $this->publicCardsQuery()->where('id', '!=', $card->id)->whereHas('tags', fn ($query) => $query->whereIn('nova_card_tags.id', $card->tags->pluck('id')))->limit(6)->get()
|
|
: collect();
|
|
$moreFromCreator = $this->publicCardsQuery()->where('user_id', $card->user_id)->where('id', '!=', $card->id)->limit(6)->get();
|
|
|
|
// v3: smart related cards using relatedness service.
|
|
$smartRelated = $this->relatedService->related($card, 8);
|
|
|
|
return view('cards.show', [
|
|
'card' => $this->presenter->card($card, true, $request->user()),
|
|
'meta' => [
|
|
'title' => $card->title . ' - Nova Cards - Skinbase Nova',
|
|
'description' => $card->description ?: $card->quote_text,
|
|
'canonical' => route('cards.show', ['slug' => $card->slug, 'id' => $card->id]),
|
|
'robots' => $card->visibility === NovaCard::VISIBILITY_PUBLIC ? 'index,follow' : 'noindex,follow',
|
|
],
|
|
'relatedByCategory' => $this->presenter->cards($relatedByCategory),
|
|
'relatedByTags' => $this->presenter->cards($relatedByTags),
|
|
'moreFromCreator' => $this->presenter->cards($moreFromCreator),
|
|
'smartRelated' => $this->presenter->cards($smartRelated->all()),
|
|
'challengeEntries' => $card->challengeEntries()
|
|
->with('challenge')
|
|
->whereNotIn('status', [\App\Models\NovaCardChallengeEntry::STATUS_HIDDEN, \App\Models\NovaCardChallengeEntry::STATUS_REJECTED])
|
|
->latest()
|
|
->limit(4)
|
|
->get(),
|
|
'comments' => $this->comments->mapComments($card, $request->user()),
|
|
]);
|
|
}
|
|
|
|
private function publicCardsQuery()
|
|
{
|
|
return NovaCard::query()
|
|
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags', 'originalCard', 'rootCard'])
|
|
->publiclyVisible();
|
|
}
|
|
|
|
private function buildCreatorSummary(User $user): array
|
|
{
|
|
$aggregate = $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->selectRaw('COUNT(*) as total_cards')
|
|
->selectRaw('SUM(CASE WHEN featured = 1 THEN 1 ELSE 0 END) as total_featured_cards')
|
|
->selectRaw('COALESCE(SUM(views_count), 0) as total_views')
|
|
->selectRaw('COALESCE(SUM(likes_count), 0) as total_likes')
|
|
->selectRaw('COALESCE(SUM(saves_count), 0) as total_saves')
|
|
->selectRaw('COALESCE(SUM(remixes_count), 0) as total_remixes')
|
|
->selectRaw('COALESCE(SUM(challenge_entries_count), 0) as total_challenge_entries')
|
|
->first();
|
|
|
|
$topStyles = $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->whereNotNull('style_family')
|
|
->select('style_family')
|
|
->selectRaw('COUNT(*) as cards_count')
|
|
->groupBy('style_family')
|
|
->orderByDesc('cards_count')
|
|
->limit(4)
|
|
->get()
|
|
->map(fn (NovaCard $card): array => [
|
|
'key' => (string) $card->style_family,
|
|
'label' => str($card->style_family)->replace('-', ' ')->title()->toString(),
|
|
'cards_count' => (int) $card->cards_count,
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$topCategories = NovaCardCategory::query()
|
|
->whereHas('cards', fn ($query) => $query->publiclyVisible()->where('user_id', $user->id))
|
|
->withCount(['cards as creator_cards_count' => fn ($query) => $query->publiclyVisible()->where('user_id', $user->id)])
|
|
->orderByDesc('creator_cards_count')
|
|
->orderBy('name')
|
|
->limit(4)
|
|
->get()
|
|
->map(fn (NovaCardCategory $category): array => [
|
|
'slug' => (string) $category->slug,
|
|
'name' => (string) $category->name,
|
|
'cards_count' => (int) $category->creator_cards_count,
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$tagCounts = NovaCardTag::query()
|
|
->select('nova_card_tags.id', 'nova_card_tags.slug', 'nova_card_tags.name')
|
|
->join('nova_card_tag_relation', 'nova_card_tag_relation.tag_id', '=', 'nova_card_tags.id')
|
|
->join('nova_cards', 'nova_cards.id', '=', 'nova_card_tag_relation.card_id')
|
|
->where('nova_cards.user_id', $user->id)
|
|
->where('nova_cards.status', NovaCard::STATUS_PUBLISHED)
|
|
->where('nova_cards.visibility', NovaCard::VISIBILITY_PUBLIC)
|
|
->whereNotIn('nova_cards.moderation_status', [NovaCard::MOD_FLAGGED, NovaCard::MOD_REJECTED])
|
|
->selectRaw('COUNT(*) as cards_count')
|
|
->groupBy('nova_card_tags.id', 'nova_card_tags.slug', 'nova_card_tags.name')
|
|
->orderByDesc(DB::raw('COUNT(*)'))
|
|
->orderBy('nova_card_tags.name')
|
|
->get();
|
|
|
|
$topTags = $tagCounts
|
|
->take(6)
|
|
->map(fn (NovaCardTag $tag): array => [
|
|
'slug' => (string) $tag->slug,
|
|
'name' => (string) $tag->name,
|
|
'cards_count' => (int) $tag->cards_count,
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$topPalettes = $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->whereNotNull('palette_family')
|
|
->select('palette_family')
|
|
->selectRaw('COUNT(*) as cards_count')
|
|
->groupBy('palette_family')
|
|
->orderByDesc('cards_count')
|
|
->limit(4)
|
|
->get()
|
|
->map(fn (NovaCard $card): array => [
|
|
'key' => (string) $card->palette_family,
|
|
'label' => str($card->palette_family)->replace('-', ' ')->title()->toString(),
|
|
'cards_count' => (int) $card->cards_count,
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$topMoods = $this->moodFamilies()
|
|
->map(function (array $mood) use ($tagCounts): array {
|
|
$count = $tagCounts
|
|
->filter(fn (NovaCardTag $tag): bool => in_array((string) $tag->slug, $mood['tag_slugs'], true))
|
|
->sum(fn (NovaCardTag $tag): int => (int) $tag->cards_count);
|
|
|
|
return [
|
|
'key' => $mood['key'],
|
|
'label' => $mood['label'],
|
|
'cards_count' => $count,
|
|
];
|
|
})
|
|
->filter(fn (array $mood): bool => (int) $mood['cards_count'] > 0)
|
|
->sortByDesc('cards_count')
|
|
->take(4)
|
|
->values()
|
|
->all();
|
|
|
|
return [
|
|
'creator' => [
|
|
'username' => (string) $user->username,
|
|
'name' => $user->name,
|
|
'display_name' => $user->name ?: '@' . $user->username,
|
|
],
|
|
'stats' => [
|
|
'total_cards' => (int) ($aggregate->total_cards ?? 0),
|
|
'total_featured_cards' => (int) ($aggregate->total_featured_cards ?? 0),
|
|
'total_views' => (int) ($aggregate->total_views ?? 0),
|
|
'total_likes' => (int) ($aggregate->total_likes ?? 0),
|
|
'total_saves' => (int) ($aggregate->total_saves ?? 0),
|
|
'total_remixes' => (int) ($aggregate->total_remixes ?? 0),
|
|
'total_challenge_entries' => (int) ($aggregate->total_challenge_entries ?? 0),
|
|
],
|
|
'top_styles' => $topStyles,
|
|
'top_palettes' => $topPalettes,
|
|
'top_moods' => $topMoods,
|
|
'top_categories' => $topCategories,
|
|
'top_tags' => $topTags,
|
|
];
|
|
}
|
|
|
|
private function creatorFeaturedWorks(User $user)
|
|
{
|
|
return $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->where('featured', true)
|
|
->orderByDesc('featured_score')
|
|
->orderByDesc('published_at')
|
|
->limit(4)
|
|
->get();
|
|
}
|
|
|
|
private function creatorHighlights(User $user, bool $excludeFeatured = false)
|
|
{
|
|
$query = $this->publicCardsQuery()
|
|
->where('user_id', $user->id);
|
|
|
|
if ($excludeFeatured) {
|
|
$query->where('featured', false);
|
|
}
|
|
|
|
$query
|
|
->orderByDesc('featured')
|
|
->orderByDesc('featured_score')
|
|
->orderByDesc('saves_count')
|
|
->orderByDesc('likes_count')
|
|
->orderByDesc('views_count')
|
|
->limit(4);
|
|
|
|
return $query->get();
|
|
}
|
|
|
|
private function creatorMostRemixedWorks(User $user)
|
|
{
|
|
return $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->where('remixes_count', '>', 0)
|
|
->orderByDesc('remixes_count')
|
|
->orderByDesc('saves_count')
|
|
->orderByDesc('likes_count')
|
|
->orderByDesc('published_at')
|
|
->limit(4)
|
|
->get();
|
|
}
|
|
|
|
private function creatorMostLikedWorks(User $user)
|
|
{
|
|
return $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->where(function ($query): void {
|
|
$query->where('likes_count', '>', 0)
|
|
->orWhere('saves_count', '>', 0);
|
|
})
|
|
->orderByDesc('likes_count')
|
|
->orderByDesc('saves_count')
|
|
->orderByDesc('views_count')
|
|
->orderByDesc('published_at')
|
|
->limit(4)
|
|
->get();
|
|
}
|
|
|
|
private function creatorRemixActivity(User $user, ?User $viewer = null): array
|
|
{
|
|
$cards = $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->where(function ($query): void {
|
|
$query->where('remixes_count', '>', 0)
|
|
->orWhereNotNull('original_card_id');
|
|
})
|
|
->orderByDesc('remixes_count')
|
|
->orderByDesc('published_at')
|
|
->limit(6)
|
|
->get();
|
|
|
|
$branchCards = $cards
|
|
->map(function (NovaCard $card) use ($viewer): array {
|
|
$presented = $this->presenter->card($card, false, $viewer);
|
|
$branchType = $card->original_card_id ? 'Published remix' : 'Community branch';
|
|
$sourceLabel = $card->originalCard?->title ?? $card->rootCard?->title ?? $card->title;
|
|
|
|
return [
|
|
'card' => $presented,
|
|
'branch_type' => $branchType,
|
|
'source_label' => $sourceLabel,
|
|
'lineage_url' => route('cards.lineage', ['slug' => $card->slug, 'id' => $card->id]),
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
|
|
return [
|
|
'total_cards_remixed_by_community' => $cards->whereNull('original_card_id')->filter(fn (NovaCard $card): bool => (int) $card->remixes_count > 0)->count(),
|
|
'total_published_remixes' => $cards->whereNotNull('original_card_id')->count(),
|
|
'branches' => $branchCards,
|
|
];
|
|
}
|
|
|
|
private function creatorRemixGraph(User $user): array
|
|
{
|
|
$cards = $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->where(function ($query): void {
|
|
$query->where('remixes_count', '>', 0)
|
|
->orWhereNotNull('original_card_id')
|
|
->orWhereNotNull('root_card_id');
|
|
})
|
|
->get();
|
|
|
|
$branches = $cards
|
|
->groupBy(fn (NovaCard $card): string => (string) ($card->root_card_id ?: $card->id))
|
|
->map(function ($group): array {
|
|
/** @var NovaCard $root */
|
|
$root = $group->firstWhere('id', $group->first()->root_card_id ?: $group->first()->id) ?? $group->first();
|
|
$peak = $group->sortByDesc('remixes_count')->first();
|
|
|
|
return [
|
|
'root_title' => (string) $root->title,
|
|
'cards_count' => $group->count(),
|
|
'total_remixes' => (int) $group->sum('remixes_count'),
|
|
'peak_title' => (string) ($peak?->title ?? $root->title),
|
|
];
|
|
})
|
|
->sortByDesc('total_remixes')
|
|
->take(4)
|
|
->values();
|
|
|
|
$maxRemixes = max(1, (int) $branches->max('total_remixes'));
|
|
|
|
return $branches
|
|
->map(fn (array $branch): array => [
|
|
...$branch,
|
|
'width_percent' => (int) max(16, round(($branch['total_remixes'] / $maxRemixes) * 100)),
|
|
])
|
|
->all();
|
|
}
|
|
|
|
private function creatorPreferenceSignals(User $user): array
|
|
{
|
|
$topFormats = $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->select('format')
|
|
->selectRaw('COUNT(*) as cards_count')
|
|
->groupBy('format')
|
|
->orderByDesc('cards_count')
|
|
->limit(3)
|
|
->get()
|
|
->map(fn (NovaCard $card): array => [
|
|
'key' => (string) $card->format,
|
|
'label' => str($card->format)->replace('-', ' ')->title()->toString(),
|
|
'cards_count' => (int) $card->cards_count,
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$topTemplates = NovaCardTemplate::query()
|
|
->whereHas('cards', fn ($query) => $query->publiclyVisible()->where('user_id', $user->id))
|
|
->withCount(['cards as creator_cards_count' => fn ($query) => $query->publiclyVisible()->where('user_id', $user->id)])
|
|
->orderByDesc('creator_cards_count')
|
|
->orderBy('name')
|
|
->limit(3)
|
|
->get()
|
|
->map(fn (NovaCardTemplate $template): array => [
|
|
'name' => (string) $template->name,
|
|
'cards_count' => (int) $template->creator_cards_count,
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$editorModes = $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->whereNotNull('editor_mode_last_used')
|
|
->select('editor_mode_last_used')
|
|
->selectRaw('COUNT(*) as cards_count')
|
|
->groupBy('editor_mode_last_used')
|
|
->orderByDesc('cards_count')
|
|
->get();
|
|
|
|
$presetCounts = NovaCardCreatorPreset::query()
|
|
->where('user_id', $user->id)
|
|
->select('preset_type')
|
|
->selectRaw('COUNT(*) as presets_count')
|
|
->groupBy('preset_type')
|
|
->orderByDesc('presets_count')
|
|
->get()
|
|
->map(fn (NovaCardCreatorPreset $preset): array => [
|
|
'type' => (string) $preset->preset_type,
|
|
'label' => str($preset->preset_type)->replace('-', ' ')->title()->toString(),
|
|
'presets_count' => (int) $preset->presets_count,
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
return [
|
|
'top_formats' => $topFormats,
|
|
'top_templates' => $topTemplates,
|
|
'preferred_editor_mode' => $editorModes->isNotEmpty()
|
|
? [
|
|
'key' => (string) $editorModes->first()->editor_mode_last_used,
|
|
'label' => (string) str((string) $editorModes->first()->editor_mode_last_used)->replace('-', ' ')->title(),
|
|
'cards_count' => (int) $editorModes->first()->cards_count,
|
|
]
|
|
: null,
|
|
'preset_counts' => $presetCounts,
|
|
];
|
|
}
|
|
|
|
private function creatorTimeline(User $user, ?User $viewer = null): array
|
|
{
|
|
return $this->publicCardsQuery()
|
|
->where('user_id', $user->id)
|
|
->latest('published_at')
|
|
->latest('id')
|
|
->limit(6)
|
|
->get()
|
|
->map(function (NovaCard $card) use ($viewer): array {
|
|
$presented = $this->presenter->card($card, false, $viewer);
|
|
$signals = [];
|
|
|
|
if ($card->featured) {
|
|
$signals[] = 'Featured release';
|
|
}
|
|
|
|
if ((int) $card->remixes_count > 0) {
|
|
$signals[] = 'Remix traction';
|
|
}
|
|
|
|
if ((int) $card->likes_count > 0 || (int) $card->saves_count > 0) {
|
|
$signals[] = 'Audience favorite';
|
|
}
|
|
|
|
return [
|
|
'card' => $presented,
|
|
'signals' => $signals,
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function creatorChallengeHistory(User $user): array
|
|
{
|
|
return NovaCardChallengeEntry::query()
|
|
->with(['challenge', 'card.user'])
|
|
->where('user_id', $user->id)
|
|
->whereNotIn('status', [NovaCardChallengeEntry::STATUS_HIDDEN, NovaCardChallengeEntry::STATUS_REJECTED])
|
|
->whereHas('card', fn ($query) => $query->publiclyVisible()->where('user_id', $user->id))
|
|
->whereHas('challenge')
|
|
->orderByRaw("CASE status WHEN 'winner' THEN 0 WHEN 'featured' THEN 1 WHEN 'submitted' THEN 2 WHEN 'active' THEN 3 ELSE 4 END")
|
|
->latest('id')
|
|
->limit(4)
|
|
->get()
|
|
->map(fn (NovaCardChallengeEntry $entry): array => [
|
|
'status_label' => match ((string) $entry->status) {
|
|
NovaCardChallengeEntry::STATUS_WINNER => 'Winner entry',
|
|
NovaCardChallengeEntry::STATUS_FEATURED => 'Featured entry',
|
|
NovaCardChallengeEntry::STATUS_ACTIVE => 'Active entry',
|
|
default => 'Submitted entry',
|
|
},
|
|
'challenge_title' => (string) ($entry->challenge?->title ?? 'Challenge'),
|
|
'challenge_url' => $entry->challenge ? route('cards.challenges.show', ['slug' => $entry->challenge->slug]) : null,
|
|
'challenge_status' => (string) ($entry->challenge?->status ?? ''),
|
|
'card_title' => (string) ($entry->card?->title ?? 'Card'),
|
|
'card_url' => $entry->card ? $entry->card->publicUrl() : null,
|
|
'official' => (bool) ($entry->challenge?->official ?? false),
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function cardsTaggedWithSlugs(array $tagSlugs)
|
|
{
|
|
$tagSlugs = collect($tagSlugs)->filter()->unique()->values()->all();
|
|
|
|
return $this->publicCardsQuery()->whereHas('tags', fn ($query) => $query->whereIn('nova_card_tags.slug', $tagSlugs));
|
|
}
|
|
|
|
private function moodFamilies()
|
|
{
|
|
return collect((array) config('nova_cards.mood_families', []))
|
|
->map(fn (array $mood): array => [
|
|
'key' => (string) ($mood['key'] ?? ''),
|
|
'label' => (string) ($mood['label'] ?? str((string) ($mood['key'] ?? ''))->replace('-', ' ')->title()),
|
|
'tag_slugs' => array_values((array) ($mood['tag_slugs'] ?? [])),
|
|
])
|
|
->filter(fn (array $mood): bool => $mood['key'] !== '')
|
|
->values();
|
|
}
|
|
|
|
private function styleFamilies()
|
|
{
|
|
return collect((array) config('nova_cards.style_families', []))
|
|
->map(fn (array $style): array => [
|
|
'key' => (string) ($style['key'] ?? ''),
|
|
'label' => (string) ($style['label'] ?? str((string) ($style['key'] ?? ''))->replace('-', ' ')->title()),
|
|
])
|
|
->filter(fn (array $style): bool => $style['key'] !== '')
|
|
->values();
|
|
}
|
|
|
|
private function paletteFamilies()
|
|
{
|
|
return collect((array) config('nova_cards.palette_families', []))
|
|
->map(fn (array $palette): array => [
|
|
'key' => (string) ($palette['key'] ?? ''),
|
|
'label' => (string) ($palette['label'] ?? str((string) ($palette['key'] ?? ''))->replace('-', ' ')->title()),
|
|
])
|
|
->filter(fn (array $palette): bool => $palette['key'] !== '')
|
|
->values();
|
|
}
|
|
|
|
private function seasonalHubs()
|
|
{
|
|
return collect((array) config('nova_cards.seasonal_hubs', []))
|
|
->map(fn (array $hub): array => [
|
|
'key' => (string) ($hub['key'] ?? ''),
|
|
'label' => (string) ($hub['label'] ?? str((string) ($hub['key'] ?? ''))->replace('-', ' ')->title()),
|
|
'tag_slugs' => array_values((array) ($hub['tag_slugs'] ?? [])),
|
|
])
|
|
->filter(fn (array $hub): bool => $hub['key'] !== '')
|
|
->values();
|
|
}
|
|
}
|