Improve creator studio browsing and versioning
This commit is contained in:
@@ -7,8 +7,6 @@ namespace App\Services;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkVersion;
|
||||
use App\Models\ArtworkVersionEvent;
|
||||
use App\Models\User;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -40,6 +38,148 @@ final class ArtworkVersioningService
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Capture the current artwork media state as a revision snapshot.
|
||||
*
|
||||
* @return array{artwork: array<string, mixed>, files: array<int, array<string, mixed>>}
|
||||
*/
|
||||
public function captureArtworkSnapshot(Artwork $artwork): array
|
||||
{
|
||||
return [
|
||||
'artwork' => [
|
||||
'file_name' => (string) ($artwork->file_name ?? ''),
|
||||
'file_path' => (string) ($artwork->file_path ?? ''),
|
||||
'hash' => (string) ($artwork->hash ?? ''),
|
||||
'file_ext' => (string) ($artwork->file_ext ?? ''),
|
||||
'thumb_ext' => (string) ($artwork->thumb_ext ?? ''),
|
||||
'file_size' => (int) ($artwork->file_size ?? 0),
|
||||
'mime_type' => (string) ($artwork->mime_type ?? 'application/octet-stream'),
|
||||
'width' => (int) ($artwork->width ?? 0),
|
||||
'height' => (int) ($artwork->height ?? 0),
|
||||
],
|
||||
'files' => DB::table('artwork_files')
|
||||
->where('artwork_id', $artwork->id)
|
||||
->orderBy('variant')
|
||||
->get(['variant', 'path', 'mime', 'size'])
|
||||
->map(fn ($row): array => [
|
||||
'variant' => (string) ($row->variant ?? ''),
|
||||
'path' => (string) ($row->path ?? ''),
|
||||
'mime' => (string) ($row->mime ?? 'application/octet-stream'),
|
||||
'size' => (int) ($row->size ?? 0),
|
||||
])
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new immutable revision from a fully materialized artwork snapshot.
|
||||
*
|
||||
* @param array<string, mixed> $snapshot
|
||||
* @param array<string, mixed>|null $previousSnapshot
|
||||
*/
|
||||
public function createVersionFromSnapshot(
|
||||
Artwork $artwork,
|
||||
array $snapshot,
|
||||
int $userId,
|
||||
?string $changeNote = null,
|
||||
?array $previousSnapshot = null,
|
||||
): ArtworkVersion {
|
||||
$normalizedSnapshot = $this->normalizeSnapshot($snapshot);
|
||||
$previous = $this->normalizeSnapshot($previousSnapshot ?? $this->captureArtworkSnapshot($artwork));
|
||||
|
||||
$this->rateLimitCheck($userId, $artwork->id);
|
||||
|
||||
return DB::transaction(function () use ($artwork, $normalizedSnapshot, $previous, $userId, $changeNote): ArtworkVersion {
|
||||
$this->ensureBaselineVersion($artwork, $previous, $userId);
|
||||
|
||||
$nextNumber = max((int) $artwork->versions()->max('version_number'), 0) + 1;
|
||||
$meta = $this->snapshotArtworkMeta($normalizedSnapshot);
|
||||
|
||||
$artwork->versions()->update(['is_current' => false]);
|
||||
|
||||
$version = ArtworkVersion::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'version_number' => $nextNumber,
|
||||
'file_path' => $meta['file_path'],
|
||||
'file_hash' => $meta['hash'],
|
||||
'width' => $meta['width'],
|
||||
'height' => $meta['height'],
|
||||
'file_size' => $meta['file_size'],
|
||||
'change_note' => $changeNote,
|
||||
'snapshot_json' => $normalizedSnapshot,
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
$needsReapproval = $this->shouldRequireReapprovalFromSnapshots($previous, $normalizedSnapshot);
|
||||
|
||||
$artwork->update([
|
||||
'current_version_id' => $version->id,
|
||||
'version_count' => $nextNumber,
|
||||
'version_updated_at' => now(),
|
||||
'requires_reapproval' => $needsReapproval,
|
||||
]);
|
||||
|
||||
$this->applyRankingProtection($artwork);
|
||||
|
||||
ArtworkVersionEvent::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $userId,
|
||||
'action' => 'create_version',
|
||||
'version_id' => $version->id,
|
||||
]);
|
||||
|
||||
$this->incrementRateLimitCounters($userId, $artwork->id);
|
||||
|
||||
return $version;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a stored snapshot back onto the artwork row and artwork_files table.
|
||||
*
|
||||
* @param array<string, mixed> $snapshot
|
||||
*/
|
||||
public function applySnapshot(Artwork $artwork, array $snapshot): void
|
||||
{
|
||||
$normalizedSnapshot = $this->normalizeSnapshot($snapshot);
|
||||
$meta = $this->snapshotArtworkMeta($normalizedSnapshot);
|
||||
|
||||
DB::transaction(function () use ($artwork, $normalizedSnapshot, $meta): void {
|
||||
DB::table('artwork_files')
|
||||
->where('artwork_id', $artwork->id)
|
||||
->delete();
|
||||
|
||||
$rows = collect($normalizedSnapshot['files'] ?? [])
|
||||
->filter(fn (array $row): bool => ($row['variant'] ?? '') !== '' && ($row['path'] ?? '') !== '')
|
||||
->map(fn (array $row): array => [
|
||||
'artwork_id' => $artwork->id,
|
||||
'variant' => (string) $row['variant'],
|
||||
'path' => (string) $row['path'],
|
||||
'mime' => (string) ($row['mime'] ?? 'application/octet-stream'),
|
||||
'size' => (int) ($row['size'] ?? 0),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($rows !== []) {
|
||||
DB::table('artwork_files')->insert($rows);
|
||||
}
|
||||
|
||||
$artwork->update([
|
||||
'file_name' => $meta['file_name'],
|
||||
'file_path' => $meta['file_path'],
|
||||
'hash' => $meta['hash'],
|
||||
'file_ext' => $meta['file_ext'],
|
||||
'thumb_ext' => $meta['thumb_ext'],
|
||||
'file_size' => $meta['file_size'],
|
||||
'mime_type' => $meta['mime_type'],
|
||||
'width' => $meta['width'],
|
||||
'height' => $meta['height'],
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new version for an artwork after a file replacement.
|
||||
*
|
||||
@@ -67,63 +207,22 @@ final class ArtworkVersioningService
|
||||
int $userId,
|
||||
?string $changeNote = null,
|
||||
): ArtworkVersion {
|
||||
// 1. Rate limit check
|
||||
$this->rateLimitCheck($userId, $artwork->id);
|
||||
$snapshot = [
|
||||
'artwork' => [
|
||||
'file_name' => (string) ($artwork->file_name ?? 'artwork'),
|
||||
'file_path' => $filePath,
|
||||
'hash' => $fileHash,
|
||||
'file_ext' => (string) ($artwork->file_ext ?? ''),
|
||||
'thumb_ext' => (string) ($artwork->thumb_ext ?? ''),
|
||||
'file_size' => $fileSize,
|
||||
'mime_type' => (string) ($artwork->mime_type ?? 'application/octet-stream'),
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
],
|
||||
'files' => [],
|
||||
];
|
||||
|
||||
// 2. Reject identical file
|
||||
if ($artwork->hash === $fileHash) {
|
||||
throw new \RuntimeException('The uploaded file is identical to the current version. No new version created.');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use (
|
||||
$artwork, $filePath, $fileHash, $width, $height, $fileSize, $userId, $changeNote
|
||||
): ArtworkVersion {
|
||||
// 3. Determine next version number
|
||||
$nextNumber = ($artwork->version_count ?? 1) + 1;
|
||||
|
||||
// 4. Mark all previous versions as not current
|
||||
$artwork->versions()->update(['is_current' => false]);
|
||||
|
||||
// 5. Insert new version row
|
||||
$version = ArtworkVersion::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'version_number' => $nextNumber,
|
||||
'file_path' => $filePath,
|
||||
'file_hash' => $fileHash,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'file_size' => $fileSize,
|
||||
'change_note' => $changeNote,
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
// 6. Check whether moderation re-review is required
|
||||
$needsReapproval = $this->shouldRequireReapproval($artwork, $width, $height);
|
||||
|
||||
// 7. Update artwork metadata (no engagement data touched)
|
||||
$artwork->update([
|
||||
'current_version_id' => $version->id,
|
||||
'version_count' => $nextNumber,
|
||||
'version_updated_at' => now(),
|
||||
'requires_reapproval' => $needsReapproval,
|
||||
]);
|
||||
|
||||
// 8. Ranking protection — apply small decay
|
||||
$this->applyRankingProtection($artwork);
|
||||
|
||||
// 9. Audit log
|
||||
ArtworkVersionEvent::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $userId,
|
||||
'action' => 'create_version',
|
||||
'version_id' => $version->id,
|
||||
]);
|
||||
|
||||
// 10. Increment hourly/daily counters for rate limiting
|
||||
$this->incrementRateLimitCounters($userId, $artwork->id);
|
||||
|
||||
return $version;
|
||||
});
|
||||
return $this->createVersionFromSnapshot($artwork, $snapshot, $userId, $changeNote);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,15 +238,33 @@ final class ArtworkVersioningService
|
||||
Artwork $artwork,
|
||||
int $userId,
|
||||
): ArtworkVersion {
|
||||
return $this->createNewVersion(
|
||||
$previousSnapshot = $this->captureArtworkSnapshot($artwork);
|
||||
$snapshot = is_array($version->snapshot_json)
|
||||
? $this->normalizeSnapshot($version->snapshot_json)
|
||||
: $this->normalizeSnapshot([
|
||||
'artwork' => array_merge($previousSnapshot['artwork'] ?? [], [
|
||||
'file_name' => (string) ($artwork->file_name ?? 'artwork'),
|
||||
'file_path' => $version->file_path,
|
||||
'hash' => $version->file_hash,
|
||||
'file_ext' => (string) ($artwork->file_ext ?? ''),
|
||||
'thumb_ext' => (string) ($artwork->thumb_ext ?? ''),
|
||||
'file_size' => (int) $version->file_size,
|
||||
'mime_type' => (string) ($artwork->mime_type ?? 'application/octet-stream'),
|
||||
'width' => (int) $version->width,
|
||||
'height' => (int) $version->height,
|
||||
]),
|
||||
'files' => $previousSnapshot['files'] ?? [],
|
||||
]);
|
||||
|
||||
$this->applySnapshot($artwork, $snapshot);
|
||||
$artwork->refresh();
|
||||
|
||||
return $this->createVersionFromSnapshot(
|
||||
$artwork,
|
||||
$version->file_path,
|
||||
$version->file_hash,
|
||||
(int) $version->width,
|
||||
(int) $version->height,
|
||||
(int) $version->file_size,
|
||||
$snapshot,
|
||||
$userId,
|
||||
"Restored from version {$version->version_number}",
|
||||
$previousSnapshot,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,6 +287,26 @@ final class ArtworkVersioningService
|
||||
|| $heightChange > self::DIMENSION_CHANGE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $previousSnapshot
|
||||
* @param array<string, mixed> $newSnapshot
|
||||
*/
|
||||
public function shouldRequireReapprovalFromSnapshots(array $previousSnapshot, array $newSnapshot): bool
|
||||
{
|
||||
$previous = $this->snapshotArtworkMeta($previousSnapshot);
|
||||
$next = $this->snapshotArtworkMeta($newSnapshot);
|
||||
|
||||
if (($previous['width'] ?? 0) <= 0 || ($previous['height'] ?? 0) <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$widthChange = abs($next['width'] - $previous['width']) / max($previous['width'], 1);
|
||||
$heightChange = abs($next['height'] - $previous['height']) / max($previous['height'], 1);
|
||||
|
||||
return $widthChange > self::DIMENSION_CHANGE_THRESHOLD
|
||||
|| $heightChange > self::DIMENSION_CHANGE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a small protective decay (7 %) to ranking and heat scores.
|
||||
*
|
||||
@@ -245,4 +382,103 @@ final class ArtworkVersioningService
|
||||
Cache::put($dayKey, 1, 86400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $snapshot
|
||||
*/
|
||||
private function ensureBaselineVersion(Artwork $artwork, ?array $snapshot, int $userId): ?ArtworkVersion
|
||||
{
|
||||
if ($artwork->versions()->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalizedSnapshot = $this->normalizeSnapshot($snapshot ?? $this->captureArtworkSnapshot($artwork));
|
||||
$meta = $this->snapshotArtworkMeta($normalizedSnapshot);
|
||||
$baselineNumber = max(1, (int) ($artwork->version_count ?? 1));
|
||||
|
||||
$version = ArtworkVersion::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'version_number' => $baselineNumber,
|
||||
'file_path' => $meta['file_path'],
|
||||
'file_hash' => $meta['hash'],
|
||||
'width' => $meta['width'],
|
||||
'height' => $meta['height'],
|
||||
'file_size' => $meta['file_size'],
|
||||
'change_note' => 'Baseline snapshot',
|
||||
'snapshot_json' => $normalizedSnapshot,
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
$artwork->update([
|
||||
'current_version_id' => $version->id,
|
||||
'version_count' => $baselineNumber,
|
||||
'version_updated_at' => now(),
|
||||
]);
|
||||
|
||||
ArtworkVersionEvent::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $userId,
|
||||
'action' => 'baseline_snapshot',
|
||||
'version_id' => $version->id,
|
||||
]);
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $snapshot
|
||||
* @return array{file_name: string, file_path: string, hash: string, file_ext: string, thumb_ext: string, file_size: int, mime_type: string, width: int, height: int}
|
||||
*/
|
||||
private function snapshotArtworkMeta(array $snapshot): array
|
||||
{
|
||||
$normalized = $this->normalizeSnapshot($snapshot);
|
||||
$artwork = $normalized['artwork'];
|
||||
|
||||
return [
|
||||
'file_name' => (string) ($artwork['file_name'] ?? 'artwork'),
|
||||
'file_path' => (string) ($artwork['file_path'] ?? ''),
|
||||
'hash' => (string) ($artwork['hash'] ?? ''),
|
||||
'file_ext' => (string) ($artwork['file_ext'] ?? ''),
|
||||
'thumb_ext' => (string) ($artwork['thumb_ext'] ?? ''),
|
||||
'file_size' => (int) ($artwork['file_size'] ?? 0),
|
||||
'mime_type' => (string) ($artwork['mime_type'] ?? 'application/octet-stream'),
|
||||
'width' => max(0, (int) ($artwork['width'] ?? 0)),
|
||||
'height' => max(0, (int) ($artwork['height'] ?? 0)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $snapshot
|
||||
* @return array{artwork: array<string, mixed>, files: array<int, array<string, mixed>>}
|
||||
*/
|
||||
private function normalizeSnapshot(?array $snapshot): array
|
||||
{
|
||||
$artwork = is_array($snapshot['artwork'] ?? null) ? $snapshot['artwork'] : [];
|
||||
$files = is_array($snapshot['files'] ?? null) ? $snapshot['files'] : [];
|
||||
|
||||
return [
|
||||
'artwork' => [
|
||||
'file_name' => (string) ($artwork['file_name'] ?? 'artwork'),
|
||||
'file_path' => (string) ($artwork['file_path'] ?? ''),
|
||||
'hash' => (string) ($artwork['hash'] ?? ''),
|
||||
'file_ext' => (string) ($artwork['file_ext'] ?? ''),
|
||||
'thumb_ext' => (string) ($artwork['thumb_ext'] ?? ''),
|
||||
'file_size' => (int) ($artwork['file_size'] ?? 0),
|
||||
'mime_type' => (string) ($artwork['mime_type'] ?? 'application/octet-stream'),
|
||||
'width' => max(0, (int) ($artwork['width'] ?? 0)),
|
||||
'height' => max(0, (int) ($artwork['height'] ?? 0)),
|
||||
],
|
||||
'files' => collect($files)
|
||||
->filter(fn ($file): bool => is_array($file))
|
||||
->map(fn (array $file): array => [
|
||||
'variant' => (string) ($file['variant'] ?? ''),
|
||||
'path' => (string) ($file['path'] ?? ''),
|
||||
'mime' => (string) ($file['mime'] ?? 'application/octet-stream'),
|
||||
'size' => (int) ($file['size'] ?? 0),
|
||||
])
|
||||
->filter(fn (array $file): bool => $file['variant'] !== '' && $file['path'] !== '')
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ final class CreatorStudioContentService
|
||||
$bucket = $fixedBucket ?: $this->normalizeBucket((string) ($filters['bucket'] ?? 'all'));
|
||||
$search = trim((string) ($filters['q'] ?? ''));
|
||||
$sort = $this->normalizeSort((string) ($filters['sort'] ?? 'updated_desc'));
|
||||
$contentType = (string) ($filters['content_type'] ?? 'all');
|
||||
$category = (string) ($filters['category'] ?? 'all');
|
||||
$tag = trim((string) ($filters['tag'] ?? ''));
|
||||
$visibility = (string) ($filters['visibility'] ?? 'all');
|
||||
@@ -63,7 +64,7 @@ final class CreatorStudioContentService
|
||||
|
||||
$items = $module === 'all'
|
||||
? SupportCollection::make($this->providers())->flatMap(fn (CreatorStudioProvider $provider) => $provider->items($user, $this->providerBucket($bucket), 200))
|
||||
: $this->provider($module)?->items($user, $this->providerBucket($bucket), 240) ?? SupportCollection::make();
|
||||
: $this->provider($module)?->items($user, $this->providerBucket($bucket), 0) ?? SupportCollection::make();
|
||||
|
||||
if ($bucket === 'featured') {
|
||||
$items = $items->filter(fn (array $item): bool => (bool) ($item['featured'] ?? false));
|
||||
@@ -91,6 +92,19 @@ final class CreatorStudioContentService
|
||||
});
|
||||
}
|
||||
|
||||
$artworkFilterItems = null;
|
||||
|
||||
if ($module === 'artworks') {
|
||||
$artworkFilterItems = $items->values();
|
||||
}
|
||||
|
||||
if ($module === 'artworks' && $contentType !== 'all') {
|
||||
$items = $items->filter(function (array $item) use ($contentType): bool {
|
||||
return SupportCollection::make($item['taxonomies']['content_types'] ?? [])
|
||||
->contains(fn (array $entry): bool => (string) ($entry['slug'] ?? '') === $contentType);
|
||||
});
|
||||
}
|
||||
|
||||
if ($module === 'artworks' && $category !== 'all') {
|
||||
$items = $items->filter(function (array $item) use ($category): bool {
|
||||
return SupportCollection::make($item['taxonomies']['categories'] ?? [])
|
||||
@@ -141,6 +155,7 @@ final class CreatorStudioContentService
|
||||
'bucket' => $bucket,
|
||||
'q' => $search,
|
||||
'sort' => $sort,
|
||||
'content_type' => $contentType,
|
||||
'category' => $category,
|
||||
'tag' => $tag,
|
||||
'visibility' => $visibility,
|
||||
@@ -174,12 +189,13 @@ final class CreatorStudioContentService
|
||||
['value' => 'title_asc', 'label' => 'Title A-Z'],
|
||||
],
|
||||
'advanced_filters' => $this->advancedFilters($module, $items, [
|
||||
'content_type' => $contentType,
|
||||
'category' => $category,
|
||||
'tag' => $tag,
|
||||
'visibility' => $visibility,
|
||||
'activity_state' => $activityState,
|
||||
'stale' => $stale,
|
||||
]),
|
||||
], $artworkFilterItems),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -323,34 +339,65 @@ final class CreatorStudioContentService
|
||||
/**
|
||||
* @param array<string, string> $currentFilters
|
||||
*/
|
||||
private function advancedFilters(string $module, SupportCollection $items, array $currentFilters): array
|
||||
private function advancedFilters(string $module, SupportCollection $items, array $currentFilters, ?SupportCollection $optionItems = null): array
|
||||
{
|
||||
return match ($module) {
|
||||
'artworks' => [
|
||||
[
|
||||
'key' => 'category',
|
||||
'label' => 'Category',
|
||||
'type' => 'select',
|
||||
'value' => $currentFilters['category'] ?? 'all',
|
||||
'options' => array_merge([
|
||||
['value' => 'all', 'label' => 'All categories'],
|
||||
], $items
|
||||
->flatMap(fn (array $item) => $item['taxonomies']['categories'] ?? [])
|
||||
->unique('slug')
|
||||
->sortBy('name')
|
||||
->map(fn (array $entry): array => [
|
||||
'value' => (string) ($entry['slug'] ?? ''),
|
||||
'label' => (string) ($entry['name'] ?? 'Category'),
|
||||
])->values()->all()),
|
||||
],
|
||||
[
|
||||
'key' => 'tag',
|
||||
'label' => 'Tag',
|
||||
'type' => 'search',
|
||||
'value' => $currentFilters['tag'] ?? '',
|
||||
'placeholder' => 'Filter by tag',
|
||||
],
|
||||
],
|
||||
'artworks' => (function () use ($items, $currentFilters, $optionItems): array {
|
||||
$optionItems = $optionItems ?? $items;
|
||||
$selectedContentType = $currentFilters['content_type'] ?? 'all';
|
||||
|
||||
$contentTypeOptions = array_merge([
|
||||
['value' => 'all', 'label' => 'All content types'],
|
||||
], $optionItems
|
||||
->flatMap(fn (array $item) => $item['taxonomies']['content_types'] ?? [])
|
||||
->unique('slug')
|
||||
->sortBy('name')
|
||||
->map(fn (array $entry): array => [
|
||||
'value' => (string) ($entry['slug'] ?? ''),
|
||||
'label' => (string) ($entry['name'] ?? 'Content type'),
|
||||
])->values()->all());
|
||||
|
||||
$categoryItems = $selectedContentType === 'all'
|
||||
? $optionItems
|
||||
: $optionItems->filter(function (array $item) use ($selectedContentType): bool {
|
||||
return SupportCollection::make($item['taxonomies']['content_types'] ?? [])
|
||||
->contains(fn (array $entry): bool => (string) ($entry['slug'] ?? '') === $selectedContentType);
|
||||
});
|
||||
|
||||
return [
|
||||
[
|
||||
'key' => 'content_type',
|
||||
'label' => 'Content Type',
|
||||
'type' => 'select',
|
||||
'value' => $currentFilters['content_type'] ?? 'all',
|
||||
'options' => $contentTypeOptions,
|
||||
],
|
||||
[
|
||||
'key' => 'category',
|
||||
'label' => 'Category',
|
||||
'type' => 'select',
|
||||
'value' => $currentFilters['category'] ?? 'all',
|
||||
'options' => array_merge([
|
||||
['value' => 'all', 'label' => 'All categories', 'content_type_slug' => 'all'],
|
||||
], $categoryItems
|
||||
->flatMap(fn (array $item) => $item['taxonomies']['categories'] ?? [])
|
||||
->unique('slug')
|
||||
->sortBy('name')
|
||||
->map(fn (array $entry): array => [
|
||||
'value' => (string) ($entry['slug'] ?? ''),
|
||||
'label' => (string) ($entry['name'] ?? 'Category'),
|
||||
'content_type_slug' => (string) ($entry['content_type_slug'] ?? 'all'),
|
||||
])->values()->all()),
|
||||
],
|
||||
[
|
||||
'key' => 'tag',
|
||||
'label' => 'Tag',
|
||||
'type' => 'search',
|
||||
'value' => $currentFilters['tag'] ?? '',
|
||||
'placeholder' => 'Filter by tag',
|
||||
],
|
||||
];
|
||||
})(),
|
||||
'collections' => [[
|
||||
'key' => 'visibility',
|
||||
'label' => 'Visibility',
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Services\Studio\Providers;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkStats;
|
||||
use App\Models\User;
|
||||
use App\Services\Artworks\ArtworkPublicationService;
|
||||
use App\Services\Studio\Contracts\CreatorStudioProvider;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -14,6 +15,10 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class ArtworkStudioProvider implements CreatorStudioProvider
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArtworkPublicationService $publicationService,
|
||||
) {}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'artworks';
|
||||
@@ -41,6 +46,8 @@ final class ArtworkStudioProvider implements CreatorStudioProvider
|
||||
|
||||
public function summary(User $user): array
|
||||
{
|
||||
$this->publicationService->publishDueScheduledForUser((int) $user->id);
|
||||
|
||||
$baseQuery = Artwork::query()->withTrashed()->where('user_id', $user->id);
|
||||
|
||||
$count = (clone $baseQuery)
|
||||
@@ -90,12 +97,15 @@ final class ArtworkStudioProvider implements CreatorStudioProvider
|
||||
|
||||
public function items(User $user, string $bucket = 'all', int $limit = 200): Collection
|
||||
{
|
||||
$this->publicationService->publishDueScheduledForUser((int) $user->id);
|
||||
|
||||
$query = Artwork::query()
|
||||
->withTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->with([
|
||||
'stats',
|
||||
'categories',
|
||||
'categories.contentType',
|
||||
'tags',
|
||||
'features' => function ($query): void {
|
||||
$query->where('is_active', true)
|
||||
@@ -106,8 +116,11 @@ final class ArtworkStudioProvider implements CreatorStudioProvider
|
||||
});
|
||||
},
|
||||
])
|
||||
->orderByDesc('updated_at')
|
||||
->limit($limit);
|
||||
->orderByDesc('updated_at');
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
if ($bucket === 'drafts') {
|
||||
$query->whereNull('deleted_at')
|
||||
@@ -134,6 +147,8 @@ final class ArtworkStudioProvider implements CreatorStudioProvider
|
||||
|
||||
public function topItems(User $user, int $limit = 5): Collection
|
||||
{
|
||||
$this->publicationService->publishDueScheduledForUser((int) $user->id);
|
||||
|
||||
return Artwork::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('deleted_at')
|
||||
@@ -226,10 +241,21 @@ final class ArtworkStudioProvider implements CreatorStudioProvider
|
||||
+ ((int) ($stats?->comments_count ?? 0) * 3)
|
||||
+ ((int) ($stats?->shares_count ?? 0) * 2),
|
||||
'taxonomies' => [
|
||||
'content_types' => $artwork->categories
|
||||
->map(fn ($entry) => $entry->contentType)
|
||||
->filter()
|
||||
->unique('id')
|
||||
->map(fn ($entry): array => [
|
||||
'id' => (int) $entry->id,
|
||||
'name' => (string) $entry->name,
|
||||
'slug' => (string) $entry->slug,
|
||||
])->values()->all(),
|
||||
'categories' => $artwork->categories->map(fn ($entry): array => [
|
||||
'id' => (int) $entry->id,
|
||||
'name' => (string) $entry->name,
|
||||
'slug' => (string) $entry->slug,
|
||||
'content_type' => (string) ($entry->contentType?->name ?? ''),
|
||||
'content_type_slug' => (string) ($entry->contentType?->slug ?? ''),
|
||||
])->values()->all(),
|
||||
'tags' => $artwork->tags->map(fn ($entry): array => [
|
||||
'id' => (int) $entry->id,
|
||||
|
||||
@@ -92,8 +92,11 @@ final class CardStudioProvider implements CreatorStudioProvider
|
||||
->withTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->with(['category', 'tags'])
|
||||
->orderByDesc('updated_at')
|
||||
->limit($limit);
|
||||
->orderByDesc('updated_at');
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
if ($bucket === 'drafts') {
|
||||
$query->whereNull('deleted_at')->where('status', NovaCard::STATUS_DRAFT);
|
||||
|
||||
@@ -101,8 +101,11 @@ final class CollectionStudioProvider implements CreatorStudioProvider
|
||||
->withTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->with(['user.profile', 'coverArtwork'])
|
||||
->orderByDesc('updated_at')
|
||||
->limit($limit);
|
||||
->orderByDesc('updated_at');
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
if ($bucket === 'drafts') {
|
||||
$query->whereNull('deleted_at')
|
||||
|
||||
@@ -83,8 +83,11 @@ final class StoryStudioProvider implements CreatorStudioProvider
|
||||
$query = Story::query()
|
||||
->where('creator_id', $user->id)
|
||||
->with(['tags'])
|
||||
->orderByDesc('updated_at')
|
||||
->limit($limit);
|
||||
->orderByDesc('updated_at');
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
if ($bucket === 'drafts') {
|
||||
$query->whereIn('status', ['draft', 'pending_review', 'rejected']);
|
||||
|
||||
Reference in New Issue
Block a user