Files
SkinbaseNova/app/Services/NovaCards/NovaCardPresenter.php
2026-03-28 19:15:39 +01:00

356 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\NovaCards;
use App\Models\NovaCard;
use App\Models\NovaCardAssetPack;
use App\Models\NovaCardCategory;
use App\Models\NovaCardCollection;
use App\Models\NovaCardChallenge;
use App\Models\NovaCardCreatorPreset;
use App\Models\NovaCardReaction;
use App\Models\NovaCardTemplate;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class NovaCardPresenter
{
public function __construct(
private readonly NovaCardProjectNormalizer $normalizer,
private readonly NovaCardPublishModerationService $moderation,
) {
}
public function options(): array
{
$assetPacks = collect((array) config('nova_cards.asset_packs', []))
->map(fn (array $pack): array => ['id' => null, ...$pack])
->values();
$templatePacks = collect((array) config('nova_cards.template_packs', []))
->map(fn (array $pack): array => ['id' => null, ...$pack])
->values();
$databasePacks = NovaCardAssetPack::query()
->where('active', true)
->orderBy('order_num')
->orderBy('name')
->get()
->map(fn (NovaCardAssetPack $pack): array => [
'id' => (int) $pack->id,
'slug' => (string) $pack->slug,
'name' => (string) $pack->name,
'description' => $pack->description,
'type' => (string) $pack->type,
'preview_image' => $pack->preview_image,
'manifest_json' => $pack->manifest_json,
'official' => (bool) $pack->official,
'active' => (bool) $pack->active,
]);
return [
'formats' => collect((array) config('nova_cards.formats', []))
->map(fn (array $format, string $key): array => [
'key' => $key,
'label' => (string) ($format['label'] ?? ucfirst($key)),
'width' => (int) ($format['width'] ?? 1080),
'height' => (int) ($format['height'] ?? 1080),
])
->values()
->all(),
'font_presets' => collect((array) config('nova_cards.font_presets', []))
->map(fn (array $font, string $key): array => ['key' => $key, ...$font])
->values()
->all(),
'gradient_presets' => collect((array) config('nova_cards.gradient_presets', []))
->map(fn (array $gradient, string $key): array => ['key' => $key, ...$gradient])
->values()
->all(),
'decor_presets' => array_values((array) config('nova_cards.decor_presets', [])),
'background_modes' => array_values((array) config('nova_cards.background_modes', [])),
'layout_presets' => array_values((array) config('nova_cards.layout_presets', [])),
'alignment_presets' => array_values((array) config('nova_cards.alignment_presets', [])),
'position_presets' => array_values((array) config('nova_cards.position_presets', [])),
'padding_presets' => array_values((array) config('nova_cards.padding_presets', [])),
'max_width_presets' => array_values((array) config('nova_cards.max_width_presets', [])),
'line_height_presets' => array_values((array) config('nova_cards.line_height_presets', [])),
'shadow_presets' => array_values((array) config('nova_cards.shadow_presets', [])),
'focal_positions' => array_values((array) config('nova_cards.focal_positions', [])),
'categories' => NovaCardCategory::query()
->where('active', true)
->orderBy('order_num')
->orderBy('name')
->get()
->map(fn (NovaCardCategory $category): array => [
'id' => (int) $category->id,
'slug' => (string) $category->slug,
'name' => (string) $category->name,
'description' => $category->description,
])
->values()
->all(),
'templates' => NovaCardTemplate::query()
->where('active', true)
->orderBy('order_num')
->orderBy('name')
->get()
->map(fn (NovaCardTemplate $template): array => [
'id' => (int) $template->id,
'slug' => (string) $template->slug,
'name' => (string) $template->name,
'description' => $template->description,
'preview_image' => $template->preview_image,
'supported_formats' => $template->supported_formats,
'config_json' => $template->config_json,
'official' => (bool) $template->official,
])
->values()
->all(),
'asset_packs' => $assetPacks
->concat($databasePacks->where('type', NovaCardAssetPack::TYPE_ASSET)->values())
->values()
->all(),
'template_packs' => $templatePacks
->concat($databasePacks->where('type', NovaCardAssetPack::TYPE_TEMPLATE)->values())
->values()
->all(),
'challenge_feed' => NovaCardChallenge::query()
->whereIn('status', [NovaCardChallenge::STATUS_ACTIVE, NovaCardChallenge::STATUS_COMPLETED])
->orderByDesc('featured')
->orderBy('starts_at')
->limit(8)
->get()
->map(fn (NovaCardChallenge $challenge): array => [
'id' => (int) $challenge->id,
'slug' => (string) $challenge->slug,
'title' => (string) $challenge->title,
'description' => $challenge->description,
'prompt' => $challenge->prompt,
'status' => (string) $challenge->status,
'official' => (bool) $challenge->official,
'featured' => (bool) $challenge->featured,
'entries_count' => (int) $challenge->entries_count,
'starts_at' => optional($challenge->starts_at)?->toISOString(),
'ends_at' => optional($challenge->ends_at)?->toISOString(),
])
->values()
->all(),
'validation' => (array) config('nova_cards.validation', []),
// v3 additions
'quote_mark_presets' => array_values((array) config('nova_cards.quote_mark_presets', [])),
'text_panel_styles' => array_values((array) config('nova_cards.text_panel_styles', [])),
'frame_presets' => array_values((array) config('nova_cards.frame_presets', [])),
'color_grade_presets' => array_values((array) config('nova_cards.color_grade_presets', [])),
'effect_presets' => array_values((array) config('nova_cards.effect_presets', [])),
'style_families' => array_values((array) config('nova_cards.style_families', [])),
'export_formats' => collect((array) config('nova_cards.export_formats', []))
->map(fn ($fmt, $key) => array_merge(['key' => $key], (array) $fmt))
->values()
->all(),
];
}
public function optionsWithPresets(array $options, User $user): array
{
$presets = NovaCardCreatorPreset::query()
->where('user_id', $user->id)
->orderByDesc('is_default')
->orderBy('name')
->get()
->groupBy('preset_type')
->map(fn ($group) => $group->map(fn ($p) => [
'id' => (int) $p->id,
'name' => (string) $p->name,
'preset_type' => (string) $p->preset_type,
'config_json' => $p->config_json ?? [],
'is_default' => (bool) $p->is_default,
])->values()->all())
->all();
return array_merge($options, ['creator_presets' => $presets]);
}
public function card(NovaCard $card, bool $withProject = false, ?User $viewer = null): array
{
$card->loadMissing(['user.profile', 'category', 'template', 'backgroundImage', 'tags', 'originalCard', 'rootCard']);
$project = $this->normalizer->normalizeForCard($card);
$viewerCollections = $viewer
? NovaCardCollection::query()
->where('user_id', $viewer->id)
->whereHas('cards', fn ($query) => $query->where('nova_cards.id', $card->id))
->pluck('id')
->map(fn ($id): int => (int) $id)
->values()
->all()
: [];
$viewerReactions = $viewer
? NovaCardReaction::query()
->where('user_id', $viewer->id)
->where('card_id', $card->id)
->pluck('type')
->all()
: [];
$moderationReasons = $this->moderation->storedReasons($card);
$moderationOverride = $this->moderation->latestOverride($card);
return [
'id' => (int) $card->id,
'uuid' => (string) $card->uuid,
'title' => (string) $card->title,
'slug' => (string) $card->slug,
'quote_text' => (string) $card->quote_text,
'quote_author' => $card->quote_author,
'quote_source' => $card->quote_source,
'description' => $card->description,
'format' => (string) $card->format,
'visibility' => (string) $card->visibility,
'status' => (string) $card->status,
'moderation_status' => (string) $card->moderation_status,
'moderation_reasons' => $moderationReasons,
'moderation_reason_labels' => $this->moderation->labelsFor($moderationReasons),
'moderation_source' => $this->moderation->storedSource($card),
'moderation_override' => $moderationOverride,
'moderation_override_history' => $this->moderation->overrideHistory($card),
'featured' => (bool) $card->featured,
'allow_download' => (bool) $card->allow_download,
'background_type' => (string) $card->background_type,
'background_image_id' => $card->background_image_id ? (int) $card->background_image_id : null,
'template_id' => $card->template_id ? (int) $card->template_id : null,
'category_id' => $card->category_id ? (int) $card->category_id : null,
'preview_url' => $card->previewUrl(),
'og_preview_url' => $card->ogPreviewUrl(),
'public_url' => $card->publicUrl(),
'published_at' => optional($card->published_at)?->toISOString(),
'render_version' => (int) $card->render_version,
'schema_version' => (int) $card->schema_version,
'views_count' => (int) $card->views_count,
'shares_count' => (int) $card->shares_count,
'downloads_count' => (int) $card->downloads_count,
'likes_count' => (int) $card->likes_count,
'favorites_count' => (int) $card->favorites_count,
'saves_count' => (int) $card->saves_count,
'remixes_count' => (int) $card->remixes_count,
'comments_count' => (int) $card->comments_count,
'challenge_entries_count' => (int) $card->challenge_entries_count,
'allow_remix' => (bool) $card->allow_remix,
'allow_background_reuse' => (bool) $card->allow_background_reuse,
'allow_export' => (bool) $card->allow_export,
'style_family' => $card->style_family,
'palette_family' => $card->palette_family,
'editor_mode_last_used' => $card->editor_mode_last_used,
'featured_score' => $card->featured_score !== null ? (float) $card->featured_score : null,
'last_engaged_at' => optional($card->last_engaged_at)?->toISOString(),
'last_ranked_at' => optional($card->last_ranked_at)?->toISOString(),
'creator' => [
'id' => (int) $card->user->id,
'username' => (string) $card->user->username,
'name' => $card->user->name,
],
'category' => $card->category ? [
'id' => (int) $card->category->id,
'slug' => (string) $card->category->slug,
'name' => (string) $card->category->name,
] : null,
'template' => $card->template ? [
'id' => (int) $card->template->id,
'slug' => (string) $card->template->slug,
'name' => (string) $card->template->name,
'description' => $card->template->description,
'config_json' => $card->template->config_json,
'supported_formats' => $card->template->supported_formats,
] : null,
'background_image' => $card->backgroundImage ? [
'id' => (int) $card->backgroundImage->id,
'processed_url' => $card->backgroundImage->processedUrl(),
'width' => (int) $card->backgroundImage->width,
'height' => (int) $card->backgroundImage->height,
] : null,
'tags' => $card->tags->map(fn ($tag): array => [
'id' => (int) $tag->id,
'slug' => (string) $tag->slug,
'name' => (string) $tag->name,
])->values()->all(),
'lineage' => [
'original_card_id' => $card->original_card_id ? (int) $card->original_card_id : null,
'root_card_id' => $card->root_card_id ? (int) $card->root_card_id : null,
'original_card' => $card->originalCard ? [
'id' => (int) $card->originalCard->id,
'title' => (string) $card->originalCard->title,
'slug' => (string) $card->originalCard->slug,
] : null,
'root_card' => $card->rootCard ? [
'id' => (int) $card->rootCard->id,
'title' => (string) $card->rootCard->title,
'slug' => (string) $card->rootCard->slug,
] : null,
],
'version_count' => $card->relationLoaded('versions') ? $card->versions->count() : $card->versions()->count(),
'can_edit' => $viewer ? $card->isOwnedBy($viewer) : false,
'viewer_state' => [
'liked' => in_array(NovaCardReaction::TYPE_LIKE, $viewerReactions, true),
'favorited' => in_array(NovaCardReaction::TYPE_FAVORITE, $viewerReactions, true),
'saved_collection_ids' => $viewerCollections,
],
'project_json' => $withProject ? $project : null,
];
}
public function cards(iterable $cards, bool $withProject = false, ?User $viewer = null): array
{
return collect($cards)
->map(fn (NovaCard $card): array => $this->card($card, $withProject, $viewer))
->values()
->all();
}
public function collection(NovaCardCollection $collection, ?User $viewer = null, bool $withCards = false): array
{
$collection->loadMissing(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags']);
$items = $collection->items
->filter(fn ($item): bool => $item->card !== null && $item->card->canBeViewedBy($viewer))
->sortBy('sort_order')
->values();
return [
'id' => (int) $collection->id,
'slug' => (string) $collection->slug,
'name' => (string) $collection->name,
'description' => $collection->description,
'visibility' => (string) $collection->visibility,
'official' => (bool) $collection->official,
'featured' => (bool) $collection->featured,
'cards_count' => (int) $collection->cards_count,
'public_url' => $collection->publicUrl(),
'owner' => [
'id' => (int) $collection->user->id,
'username' => (string) $collection->user->username,
'name' => $collection->user->name,
],
'cover_card' => $items->isNotEmpty() ? $this->card($items->first()->card, false, $viewer) : null,
'items' => $withCards ? $items->map(fn ($item): array => [
'id' => (int) $item->id,
'note' => $item->note,
'sort_order' => (int) $item->sort_order,
'card' => $this->card($item->card, false, $viewer),
])->values()->all() : [],
];
}
public function paginator(LengthAwarePaginator $paginator, bool $withProject = false, ?User $viewer = null): array
{
return [
'data' => $this->cards($paginator->items(), $withProject, $viewer),
'meta' => [
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
],
];
}
}