Studio: make grid checkbox rectangular and commit table changes
This commit is contained in:
209
app/Services/Studio/StudioArtworkQueryService.php
Normal file
209
app/Services/Studio/StudioArtworkQueryService.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Studio;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Handles artwork listing queries for Studio, using Meilisearch with DB fallback.
|
||||
*/
|
||||
final class StudioArtworkQueryService
|
||||
{
|
||||
/**
|
||||
* List artworks for a creator with search, filter, and sort via Meilisearch.
|
||||
*
|
||||
* Supported $filters keys:
|
||||
* q string — free-text search
|
||||
* status string — published|draft|archived
|
||||
* category string — category slug
|
||||
* tags array — tag slugs
|
||||
* date_from string — Y-m-d
|
||||
* date_to string — Y-m-d
|
||||
* performance string — rising|top|low
|
||||
* sort string — created_at:desc (default), ranking_score:desc, heat_score:desc, etc.
|
||||
*/
|
||||
public function list(int $userId, array $filters = [], int $perPage = 24): LengthAwarePaginator
|
||||
{
|
||||
// Skip Meilisearch when driver is null (e.g. in tests)
|
||||
$driver = config('scout.driver');
|
||||
if (empty($driver) || $driver === 'null') {
|
||||
return $this->listViaDatabase($userId, $filters, $perPage);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->listViaMeilisearch($userId, $filters, $perPage);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Studio: Meilisearch unavailable, falling back to DB', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->listViaDatabase($userId, $filters, $perPage);
|
||||
}
|
||||
}
|
||||
|
||||
private function listViaMeilisearch(int $userId, array $filters, int $perPage): LengthAwarePaginator
|
||||
{
|
||||
$q = $filters['q'] ?? '';
|
||||
$filterParts = ["author_id = {$userId}"];
|
||||
$sort = [];
|
||||
|
||||
// Status filter
|
||||
$status = $filters['status'] ?? null;
|
||||
if ($status === 'published') {
|
||||
$filterParts[] = 'is_public = true AND is_approved = true';
|
||||
} elseif ($status === 'draft') {
|
||||
$filterParts[] = 'is_public = false';
|
||||
}
|
||||
// archived handled at DB level since Meili doesn't see soft-deleted
|
||||
|
||||
// Category filter
|
||||
if (!empty($filters['category'])) {
|
||||
$filterParts[] = 'category = "' . addslashes((string) $filters['category']) . '"';
|
||||
}
|
||||
|
||||
// Tag filter
|
||||
if (!empty($filters['tags'])) {
|
||||
foreach ((array) $filters['tags'] as $tag) {
|
||||
$filterParts[] = 'tags = "' . addslashes((string) $tag) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
// Date range
|
||||
if (!empty($filters['date_from'])) {
|
||||
$filterParts[] = 'created_at >= "' . $filters['date_from'] . '"';
|
||||
}
|
||||
if (!empty($filters['date_to'])) {
|
||||
$filterParts[] = 'created_at <= "' . $filters['date_to'] . '"';
|
||||
}
|
||||
|
||||
// Performance quick filters
|
||||
if (!empty($filters['performance'])) {
|
||||
match ($filters['performance']) {
|
||||
'rising' => $filterParts[] = 'heat_score > 5',
|
||||
'top' => $filterParts[] = 'ranking_score > 50',
|
||||
'low' => $filterParts[] = 'views < 10',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Sort
|
||||
$sortParam = $filters['sort'] ?? 'created_at:desc';
|
||||
$validSortFields = [
|
||||
'created_at', 'ranking_score', 'heat_score',
|
||||
'views', 'likes', 'shares_count',
|
||||
'downloads', 'comments_count', 'favorites_count',
|
||||
];
|
||||
$parts = explode(':', $sortParam);
|
||||
if (count($parts) === 2 && in_array($parts[0], $validSortFields, true)) {
|
||||
$sort[] = $parts[0] . ':' . ($parts[1] === 'asc' ? 'asc' : 'desc');
|
||||
}
|
||||
|
||||
$options = ['filter' => implode(' AND ', $filterParts)];
|
||||
if ($sort !== []) {
|
||||
$options['sort'] = $sort;
|
||||
}
|
||||
|
||||
return Artwork::search($q ?: '')
|
||||
->options($options)
|
||||
->query(fn (Builder $query) => $query
|
||||
->with(['stats', 'categories', 'tags'])
|
||||
->withCount(['comments', 'downloads'])
|
||||
)
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
private function listViaDatabase(int $userId, array $filters, int $perPage): LengthAwarePaginator
|
||||
{
|
||||
$query = Artwork::where('user_id', $userId)
|
||||
->with(['stats', 'categories', 'tags'])
|
||||
->withCount(['comments', 'downloads']);
|
||||
|
||||
$status = $filters['status'] ?? null;
|
||||
if ($status === 'published') {
|
||||
$query->where('is_public', true)->where('is_approved', true);
|
||||
} elseif ($status === 'draft') {
|
||||
$query->where('is_public', false);
|
||||
} elseif ($status === 'archived') {
|
||||
$query->onlyTrashed();
|
||||
} else {
|
||||
// Show all except archived by default
|
||||
$query->whereNull('deleted_at');
|
||||
}
|
||||
|
||||
// Free-text search
|
||||
if (!empty($filters['q'])) {
|
||||
$q = $filters['q'];
|
||||
$query->where(function (Builder $w) use ($q) {
|
||||
$w->where('title', 'LIKE', "%{$q}%")
|
||||
->orWhereHas('tags', fn (Builder $t) => $t->where('slug', 'LIKE', "%{$q}%"));
|
||||
});
|
||||
}
|
||||
|
||||
// Category
|
||||
if (!empty($filters['category'])) {
|
||||
$query->whereHas('categories', fn (Builder $c) => $c->where('slug', $filters['category']));
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (!empty($filters['tags'])) {
|
||||
foreach ((array) $filters['tags'] as $tag) {
|
||||
$query->whereHas('tags', fn (Builder $t) => $t->where('slug', $tag));
|
||||
}
|
||||
}
|
||||
|
||||
// Date range
|
||||
if (!empty($filters['date_from'])) {
|
||||
$query->where('created_at', '>=', $filters['date_from']);
|
||||
}
|
||||
if (!empty($filters['date_to'])) {
|
||||
$query->where('created_at', '<=', $filters['date_to']);
|
||||
}
|
||||
|
||||
// Performance
|
||||
if (!empty($filters['performance'])) {
|
||||
$query->whereHas('stats', function (Builder $s) use ($filters) {
|
||||
match ($filters['performance']) {
|
||||
'rising' => $s->where('heat_score', '>', 5),
|
||||
'top' => $s->where('ranking_score', '>', 50),
|
||||
'low' => $s->where('views', '<', 10),
|
||||
default => null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Sort
|
||||
$sortParam = $filters['sort'] ?? 'created_at:desc';
|
||||
$parts = explode(':', $sortParam);
|
||||
$sortField = $parts[0] ?? 'created_at';
|
||||
$sortDir = ($parts[1] ?? 'desc') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$dbSortMap = [
|
||||
'created_at' => 'artworks.created_at',
|
||||
'ranking_score' => 'ranking_score',
|
||||
'heat_score' => 'heat_score',
|
||||
'views' => 'views',
|
||||
'likes' => 'favorites',
|
||||
'shares_count' => 'shares_count',
|
||||
'downloads' => 'downloads',
|
||||
'comments_count' => 'comments_count',
|
||||
'favorites_count' => 'favorites',
|
||||
];
|
||||
|
||||
$statsSortFields = ['ranking_score', 'heat_score', 'views', 'likes', 'shares_count', 'downloads', 'comments_count', 'favorites_count'];
|
||||
|
||||
if (in_array($sortField, $statsSortFields, true)) {
|
||||
$dbCol = $dbSortMap[$sortField] ?? $sortField;
|
||||
$query->leftJoin('artwork_stats', 'artworks.id', '=', 'artwork_stats.artwork_id')
|
||||
->orderBy("artwork_stats.{$dbCol}", $sortDir)
|
||||
->select('artworks.*');
|
||||
} else {
|
||||
$query->orderBy($dbSortMap[$sortField] ?? 'artworks.created_at', $sortDir);
|
||||
}
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user