256 lines
13 KiB
PHP
256 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\User;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Collections\CollectionSavedLibraryRequest;
|
|
use App\Models\Collection;
|
|
use App\Models\CollectionSavedList;
|
|
use App\Services\CollectionRecommendationService;
|
|
use App\Services\CollectionSavedLibraryService;
|
|
use App\Services\CollectionService;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class SavedCollectionController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly CollectionService $collections,
|
|
private readonly CollectionSavedLibraryService $savedLibrary,
|
|
private readonly CollectionRecommendationService $recommendations,
|
|
) {
|
|
}
|
|
|
|
public function index(CollectionSavedLibraryRequest $request): Response
|
|
{
|
|
return $this->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<int, string> $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<int, string> $notes
|
|
* @return array<string, int>
|
|
*/
|
|
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;
|
|
}
|
|
}
|