renderSavedLibrary($request); } public function showList(CollectionSavedLibraryRequest $request, string $listSlug): Response { $list = $this->savedLibrary->findListBySlugForUser($request->user(), $listSlug); return $this->renderSavedLibrary($request, $list); } private function renderSavedLibrary(CollectionSavedLibraryRequest $request, ?CollectionSavedList $activeList = null): Response { $savedCollections = $this->collections->getSavedCollectionsForUser($request->user(), 120); $filter = (string) ($request->validated('filter') ?? 'all'); $sort = (string) ($request->validated('sort') ?? 'saved_desc'); $query = trim((string) ($request->validated('q') ?? '')); $listId = $activeList ? (int) $activeList->id : ($request->filled('list') ? (int) $request->query('list') : null); $preserveListOrder = false; $listOrder = null; if ($activeList) { $preserveListOrder = true; $allowedCollectionIds = $this->savedLibrary->collectionIdsForList($request->user(), $activeList); $listOrder = array_flip($allowedCollectionIds); $savedCollections = $savedCollections ->filter(fn ($collection) => in_array((int) $collection->id, $allowedCollectionIds, true)) ->sortBy(fn ($collection) => $listOrder[(int) $collection->id] ?? PHP_INT_MAX) ->values(); } elseif ($listId) { $preserveListOrder = true; $activeList = $request->user()->savedCollectionLists()->withCount('items')->findOrFail($listId); $allowedCollectionIds = $this->savedLibrary->collectionIdsForList($request->user(), $activeList); $listOrder = array_flip($allowedCollectionIds); $savedCollections = $savedCollections ->filter(fn ($collection) => in_array((int) $collection->id, $allowedCollectionIds, true)) ->sortBy(fn ($collection) => $listOrder[(int) $collection->id] ?? PHP_INT_MAX) ->values(); } $savedCollectionIds = $savedCollections->pluck('id')->map(static fn ($id): int => (int) $id)->all(); $notes = $this->savedLibrary->notesFor($request->user(), $savedCollectionIds); $saveMetadata = $this->savedLibrary->saveMetadataFor($request->user(), $savedCollectionIds); $filterCounts = $this->filterCounts($savedCollections, $notes); $savedCollections = $savedCollections ->filter(fn (Collection $collection): bool => $this->matchesSearch($collection, $query)) ->filter(fn (Collection $collection): bool => $this->matchesFilter($collection, $filter, $notes)) ->values(); if (! ($preserveListOrder && $sort === 'saved_desc')) { $savedCollections = $this->sortCollections($savedCollections, $sort)->values(); } $collectionPayloads = $this->collections->mapCollectionCardPayloads($savedCollections, false); $collectionIds = collect($collectionPayloads)->pluck('id')->map(static fn ($id) => (int) $id)->all(); $memberships = $this->savedLibrary->membershipsFor($request->user(), $collectionIds); $savedLists = collect($this->savedLibrary->listsFor($request->user())) ->map(function (array $list) use ($filter, $sort, $query): array { return [ ...$list, 'url' => route('me.saved.collections.lists.show', ['listSlug' => $list['slug']]) . ($filter !== 'all' || $sort !== 'saved_desc' ? ('?' . http_build_query(array_filter([ 'filter' => $filter !== 'all' ? $filter : null, 'sort' => $sort !== 'saved_desc' ? $sort : null, 'q' => $query !== '' ? $query : null, ]))) : ''), ]; }) ->values() ->all(); $filterOptions = [ ['key' => 'all', 'label' => 'All', 'count' => $filterCounts['all'] ?? 0], ['key' => 'editorial', 'label' => 'Editorial', 'count' => $filterCounts['editorial'] ?? 0], ['key' => 'community', 'label' => 'Community', 'count' => $filterCounts['community'] ?? 0], ['key' => 'personal', 'label' => 'Personal', 'count' => $filterCounts['personal'] ?? 0], ['key' => 'seasonal', 'label' => 'Seasonal or campaign', 'count' => $filterCounts['seasonal'] ?? 0], ['key' => 'noted', 'label' => 'With notes', 'count' => $filterCounts['noted'] ?? 0], ['key' => 'revisited', 'label' => 'Revisited', 'count' => $filterCounts['revisited'] ?? 0], ]; $sortOptions = [ ['key' => 'saved_desc', 'label' => 'Recently saved'], ['key' => 'saved_asc', 'label' => 'Oldest saved'], ['key' => 'updated_desc', 'label' => 'Recently updated'], ['key' => 'revisited_desc', 'label' => 'Recently revisited'], ['key' => 'ranking_desc', 'label' => 'Highest ranking'], ['key' => 'title_asc', 'label' => 'Title A-Z'], ]; return Inertia::render('Collection/SavedCollections', [ 'collections' => collect($collectionPayloads)->map(function (array $collection) use ($memberships, $notes, $saveMetadata): array { return [ ...$collection, 'saved_list_ids' => $memberships[(int) $collection['id']] ?? [], 'saved_note' => $notes[(int) $collection['id']] ?? null, 'saved_because' => $saveMetadata[(int) $collection['id']]['saved_because'] ?? null, 'last_viewed_at' => $saveMetadata[(int) $collection['id']]['last_viewed_at'] ?? null, ]; })->all(), 'recentlyRevisited' => $this->collections->mapCollectionCardPayloads($this->savedLibrary->recentlyRevisited($request->user(), 6), false), 'recommendedCollections' => $this->collections->mapCollectionCardPayloads($this->recommendations->recommendedForUser($request->user(), 6), false), 'savedLists' => $savedLists, 'activeList' => $activeList ? [ 'id' => (int) $activeList->id, 'title' => (string) $activeList->title, 'slug' => (string) $activeList->slug, 'items_count' => (int) $activeList->items_count, 'url' => route('me.saved.collections.lists.show', ['listSlug' => $activeList->slug]), ] : null, 'activeFilters' => [ 'q' => $query, 'filter' => $filter, 'sort' => $sort, 'list' => $listId, ], 'filterOptions' => $filterOptions, 'sortOptions' => $sortOptions, 'endpoints' => [ 'createList' => route('me.saved.collections.lists.store'), 'addToListPattern' => route('me.saved.collections.lists.items.store', ['collection' => '__COLLECTION__']), 'removeFromListPattern' => route('me.saved.collections.lists.items.destroy', ['list' => '__LIST__', 'collection' => '__COLLECTION__']), 'reorderItemsPattern' => route('me.saved.collections.lists.items.reorder', ['list' => '__LIST__']), 'updateNotePattern' => route('me.saved.collections.notes.update', ['collection' => '__COLLECTION__']), 'unsavePattern' => route('collections.unsave', ['collection' => '__COLLECTION__']), ], 'libraryUrl' => route('me.saved.collections'), 'browseUrl' => route('collections.featured'), 'seo' => [ 'title' => $activeList ? sprintf('%s — Saved Collections — Skinbase Nova', $activeList->title) : 'Saved Collections — Skinbase Nova', 'description' => $activeList ? sprintf('Saved collections in the %s list on Skinbase Nova.', $activeList->title) : 'Your saved collections on Skinbase Nova.', 'canonical' => $activeList ? route('me.saved.collections.lists.show', ['listSlug' => $activeList->slug]) : route('me.saved.collections'), 'robots' => 'noindex,follow', ], ])->rootView('collections'); } private function matchesSearch(Collection $collection, string $query): bool { if ($query === '') { return true; } $haystacks = [ $collection->title, $collection->subtitle, $collection->summary, $collection->description, $collection->campaign_label, $collection->season_key, $collection->event_label, $collection->series_title, optional($collection->user)->username, optional($collection->user)->name, ]; $needle = mb_strtolower($query); return collect($haystacks) ->filter(fn ($value): bool => is_string($value) && $value !== '') ->contains(fn (string $value): bool => str_contains(mb_strtolower($value), $needle)); } /** * @param array $notes */ private function matchesFilter(Collection $collection, string $filter, array $notes): bool { return match ($filter) { 'editorial' => $collection->type === Collection::TYPE_EDITORIAL, 'community' => $collection->type === Collection::TYPE_COMMUNITY, 'personal' => $collection->type === Collection::TYPE_PERSONAL, 'seasonal' => filled($collection->season_key) || filled($collection->event_key) || filled($collection->campaign_key), 'noted' => filled($notes[(int) $collection->id] ?? null), 'revisited' => $this->timestamp($collection->saved_last_viewed_at) !== $this->timestamp($collection->saved_at), default => true, }; } /** * @param array $notes * @return array */ private function filterCounts($collections, array $notes): array { return [ 'all' => $collections->count(), 'editorial' => $collections->where('type', Collection::TYPE_EDITORIAL)->count(), 'community' => $collections->where('type', Collection::TYPE_COMMUNITY)->count(), 'personal' => $collections->where('type', Collection::TYPE_PERSONAL)->count(), 'seasonal' => $collections->filter(fn (Collection $collection): bool => filled($collection->season_key) || filled($collection->event_key) || filled($collection->campaign_key))->count(), 'noted' => $collections->filter(fn (Collection $collection): bool => filled($notes[(int) $collection->id] ?? null))->count(), 'revisited' => $collections->filter(fn (Collection $collection): bool => $this->timestamp($collection->saved_last_viewed_at) !== $this->timestamp($collection->saved_at))->count(), ]; } private function sortCollections($collections, string $sort) { return match ($sort) { 'saved_asc' => $collections->sortBy(fn (Collection $collection): int => $this->timestamp($collection->saved_at)), 'updated_desc' => $collections->sortByDesc(fn (Collection $collection): int => $this->timestamp($collection->updated_at)), 'revisited_desc' => $collections->sortByDesc(fn (Collection $collection): int => $this->timestamp($collection->saved_last_viewed_at)), 'ranking_desc' => $collections->sortByDesc(fn (Collection $collection): float => (float) ($collection->ranking_score ?? 0)), 'title_asc' => $collections->sortBy(fn (Collection $collection): string => mb_strtolower((string) $collection->title)), default => $collections->sortByDesc(fn (Collection $collection): int => $this->timestamp($collection->saved_at)), }; } private function timestamp(mixed $value): int { if ($value instanceof \DateTimeInterface) { return $value->getTimestamp(); } if (is_numeric($value)) { return (int) $value; } if (is_string($value) && $value !== '') { $timestamp = strtotime($value); return $timestamp !== false ? $timestamp : 0; } return 0; } }