optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -27,6 +27,7 @@ final class ArtworkCreateRequest extends FormRequest
'category' => 'nullable|integer|exists:categories,id',
'tags' => 'nullable|string|max:200',
'license' => 'nullable|boolean',
'is_mature' => 'nullable|boolean',
];
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class AttachCollectionArtworksRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'artwork_ids' => ['required', 'array', 'min:1'],
'artwork_ids.*' => ['integer', 'distinct'],
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Collection;
use Illuminate\Foundation\Http\FormRequest;
class CollectionBulkActionsRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'action' => ['required', 'string', 'in:archive,assign_campaign,update_lifecycle,request_ai_review,mark_editorial_review'],
'collection_ids' => ['required', 'array', 'min:1'],
'collection_ids.*' => ['integer', 'distinct'],
'campaign_key' => ['nullable', 'string', 'max:80'],
'campaign_label' => ['nullable', 'string', 'max:120'],
'lifecycle_state' => ['nullable', 'string', 'in:' . implode(',', [
Collection::LIFECYCLE_DRAFT,
Collection::LIFECYCLE_PUBLISHED,
Collection::LIFECYCLE_ARCHIVED,
])],
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator): void {
$action = (string) $this->input('action', '');
if ($action === 'assign_campaign' && blank($this->input('campaign_key'))) {
$validator->errors()->add('campaign_key', 'Campaign key is required for campaign assignment.');
}
if ($action === 'update_lifecycle' && blank($this->input('lifecycle_state'))) {
$validator->errors()->add('lifecycle_state', 'Lifecycle state is required for lifecycle updates.');
}
});
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Collection;
use Illuminate\Foundation\Http\FormRequest;
class CollectionOwnerSearchRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'q' => ['nullable', 'string', 'max:120'],
'type' => ['nullable', 'string', 'in:' . implode(',', [
Collection::TYPE_PERSONAL,
Collection::TYPE_COMMUNITY,
Collection::TYPE_EDITORIAL,
])],
'visibility' => ['nullable', 'string', 'in:' . implode(',', [
Collection::VISIBILITY_PUBLIC,
Collection::VISIBILITY_UNLISTED,
Collection::VISIBILITY_PRIVATE,
])],
'lifecycle_state' => ['nullable', 'string', 'in:' . implode(',', [
Collection::LIFECYCLE_DRAFT,
Collection::LIFECYCLE_SCHEDULED,
Collection::LIFECYCLE_PUBLISHED,
Collection::LIFECYCLE_FEATURED,
Collection::LIFECYCLE_ARCHIVED,
Collection::LIFECYCLE_HIDDEN,
Collection::LIFECYCLE_RESTRICTED,
Collection::LIFECYCLE_UNDER_REVIEW,
Collection::LIFECYCLE_EXPIRED,
])],
'mode' => ['nullable', 'string', 'in:' . implode(',', [
Collection::MODE_MANUAL,
Collection::MODE_SMART,
])],
'campaign_key' => ['nullable', 'string', 'max:80'],
'program_key' => ['nullable', 'string', 'max:80'],
'workflow_state' => ['nullable', 'string', 'in:' . implode(',', [
Collection::WORKFLOW_DRAFT,
Collection::WORKFLOW_IN_REVIEW,
Collection::WORKFLOW_APPROVED,
Collection::WORKFLOW_PROGRAMMED,
Collection::WORKFLOW_ARCHIVED,
])],
'health_state' => ['nullable', 'string', 'in:' . implode(',', [
Collection::HEALTH_HEALTHY,
Collection::HEALTH_NEEDS_METADATA,
Collection::HEALTH_STALE,
Collection::HEALTH_LOW_CONTENT,
Collection::HEALTH_BROKEN_ITEMS,
Collection::HEALTH_WEAK_COVER,
Collection::HEALTH_LOW_ENGAGEMENT,
Collection::HEALTH_ATTRIBUTION_INCOMPLETE,
Collection::HEALTH_NEEDS_REVIEW,
Collection::HEALTH_DUPLICATE_RISK,
Collection::HEALTH_MERGE_CANDIDATE,
])],
'partner_key' => ['nullable', 'string', 'max:80'],
'experiment_key' => ['nullable', 'string', 'max:80'],
'placement_eligibility' => ['nullable', 'boolean'],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class CollectionProgramAssignmentRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'collection_id' => ['required', 'integer', 'exists:collections,id'],
'program_key' => ['required', 'string', 'max:80'],
'campaign_key' => ['nullable', 'string', 'max:80'],
'placement_scope' => ['nullable', 'string', 'max:80'],
'starts_at' => ['nullable', 'date'],
'ends_at' => ['nullable', 'date', 'after:starts_at'],
'priority' => ['nullable', 'integer', 'min:-100', 'max:100'],
'notes' => ['nullable', 'string', 'max:1000'],
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class CollectionProgrammingCollectionRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'collection_id' => ['nullable', 'integer', 'exists:collections,id'],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class CollectionProgrammingMergePairRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'source_collection_id' => ['required', 'integer', 'exists:collections,id'],
'target_collection_id' => ['required', 'integer', 'exists:collections,id', 'different:source_collection_id'],
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class CollectionProgrammingMetadataRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'collection_id' => ['required', 'integer', 'exists:collections,id'],
'experiment_key' => ['nullable', 'string', 'max:80'],
'experiment_treatment' => ['nullable', 'string', 'max:80'],
'placement_variant' => ['nullable', 'string', 'max:80'],
'ranking_mode_variant' => ['nullable', 'string', 'max:80'],
'collection_pool_version' => ['nullable', 'string', 'max:80'],
'test_label' => ['nullable', 'string', 'max:120'],
'placement_eligibility' => ['nullable', 'boolean'],
'promotion_tier' => ['nullable', 'string', 'max:40'],
'partner_key' => ['nullable', 'string', 'max:80'],
'trust_tier' => ['nullable', 'string', 'max:40'],
'sponsorship_state' => ['nullable', 'string', 'max:40'],
'ownership_domain' => ['nullable', 'string', 'max:80'],
'commercial_review_state' => ['nullable', 'string', 'max:40'],
'legal_review_state' => ['nullable', 'string', 'max:40'],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class CollectionProgrammingPreviewRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'program_key' => ['required', 'string', 'max:80'],
'limit' => ['nullable', 'integer', 'min:1', 'max:24'],
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class CollectionSavedLibraryRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'q' => ['nullable', 'string', 'max:120'],
'filter' => ['nullable', 'string', 'in:all,editorial,community,personal,seasonal,noted,revisited'],
'sort' => ['nullable', 'string', 'in:saved_desc,saved_asc,updated_desc,revisited_desc,ranking_desc,title_asc'],
'list' => ['nullable', 'integer', 'min:1'],
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Collection;
use Illuminate\Foundation\Http\FormRequest;
class CollectionTargetActionRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'target_collection_id' => ['required', 'integer', 'exists:collections,id'],
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator): void {
$targetCollectionId = (int) $this->input('target_collection_id');
$collection = $this->route('collection');
if ($collection instanceof Collection && $targetCollectionId === (int) $collection->id) {
$validator->errors()->add('target_collection_id', 'Choose a different target collection.');
}
});
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class ReorderCollectionArtworksRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'ordered_artwork_ids' => ['required', 'array'],
'ordered_artwork_ids.*' => ['integer', 'distinct'],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class ReorderProfileCollectionsRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'collection_ids' => ['required', 'array', 'min:1'],
'collection_ids.*' => ['required', 'integer'],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class ReorderSavedCollectionListItemsRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'collection_ids' => ['required', 'array', 'min:1'],
'collection_ids.*' => ['required', 'integer', 'distinct'],
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class SmartCollectionRulesRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'smart_rules_json' => ['required', 'array'],
'smart_rules_json.match' => ['nullable', 'string'],
'smart_rules_json.sort' => ['nullable', 'string'],
'smart_rules_json.rules' => ['required', 'array'],
];
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Collection;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
class StoreCollectionRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
$title = (string) $this->input('title', '');
$slug = (string) $this->input('slug', '');
$mode = (string) ($this->input('mode') ?: Collection::MODE_MANUAL);
$sortMode = (string) ($this->input('sort_mode') ?: ($mode === Collection::MODE_SMART ? Collection::SORT_NEWEST : Collection::SORT_MANUAL));
if ($slug === '' && $title !== '') {
$slug = Str::slug(Str::limit($title, 140, ''));
}
$this->merge([
'slug' => $slug,
'mode' => $mode,
'sort_mode' => $sortMode,
]);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:2', 'max:120'],
'slug' => ['required', 'string', 'min:2', 'max:140', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
'type' => ['nullable', 'in:' . implode(',', [
Collection::TYPE_PERSONAL,
Collection::TYPE_COMMUNITY,
Collection::TYPE_EDITORIAL,
])],
'editorial_owner_mode' => ['nullable', 'in:' . implode(',', [
Collection::EDITORIAL_OWNER_CREATOR,
Collection::EDITORIAL_OWNER_STAFF_ACCOUNT,
Collection::EDITORIAL_OWNER_SYSTEM,
])],
'editorial_owner_username' => ['nullable', 'string', 'max:60'],
'editorial_owner_label' => ['nullable', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:1000'],
'subtitle' => ['nullable', 'string', 'max:160'],
'summary' => ['nullable', 'string', 'max:320'],
'lifecycle_state' => ['nullable', 'in:' . implode(',', [
Collection::LIFECYCLE_DRAFT,
Collection::LIFECYCLE_SCHEDULED,
Collection::LIFECYCLE_PUBLISHED,
Collection::LIFECYCLE_FEATURED,
Collection::LIFECYCLE_ARCHIVED,
Collection::LIFECYCLE_EXPIRED,
])],
'collaboration_mode' => ['nullable', 'in:' . implode(',', [
Collection::COLLABORATION_CLOSED,
Collection::COLLABORATION_INVITE_ONLY,
Collection::COLLABORATION_OPEN,
])],
'allow_submissions' => ['nullable', 'boolean'],
'allow_comments' => ['nullable', 'boolean'],
'allow_saves' => ['nullable', 'boolean'],
'event_key' => ['nullable', 'string', 'max:80'],
'event_label' => ['nullable', 'string', 'max:120'],
'season_key' => ['nullable', 'string', 'max:80'],
'banner_text' => ['nullable', 'string', 'max:200'],
'badge_label' => ['nullable', 'string', 'max:80'],
'spotlight_style' => ['nullable', 'in:' . implode(',', [
Collection::SPOTLIGHT_STYLE_DEFAULT,
Collection::SPOTLIGHT_STYLE_EDITORIAL,
Collection::SPOTLIGHT_STYLE_SEASONAL,
Collection::SPOTLIGHT_STYLE_CHALLENGE,
Collection::SPOTLIGHT_STYLE_COMMUNITY,
])],
'analytics_enabled' => ['nullable', 'boolean'],
'presentation_style' => ['nullable', 'in:' . implode(',', [
Collection::PRESENTATION_STANDARD,
Collection::PRESENTATION_EDITORIAL_GRID,
Collection::PRESENTATION_HERO_GRID,
Collection::PRESENTATION_MASONRY,
])],
'emphasis_mode' => ['nullable', 'in:' . implode(',', [
Collection::EMPHASIS_COVER_HEAVY,
Collection::EMPHASIS_BALANCED,
Collection::EMPHASIS_ARTWORK_FIRST,
])],
'theme_token' => ['nullable', 'in:default,subtle-blue,violet,amber'],
'series_key' => ['nullable', 'string', 'max:80'],
'series_title' => ['nullable', 'string', 'max:160'],
'series_description' => ['nullable', 'string', 'max:400'],
'series_order' => ['nullable', 'integer', 'min:1', 'max:9999'],
'campaign_key' => ['nullable', 'string', 'max:80'],
'campaign_label' => ['nullable', 'string', 'max:120'],
'commercial_eligibility' => ['nullable', 'boolean'],
'promotion_tier' => ['nullable', 'string', 'max:40'],
'sponsorship_label' => ['nullable', 'string', 'max:120'],
'partner_label' => ['nullable', 'string', 'max:120'],
'monetization_ready_status' => ['nullable', 'string', 'max:40'],
'brand_safe_status' => ['nullable', 'string', 'max:40'],
'published_at' => ['nullable', 'date'],
'unpublished_at' => ['nullable', 'date', 'after:published_at'],
'archived_at' => ['nullable', 'date'],
'expired_at' => ['nullable', 'date'],
'visibility' => ['required', 'in:' . implode(',', [
Collection::VISIBILITY_PUBLIC,
Collection::VISIBILITY_UNLISTED,
Collection::VISIBILITY_PRIVATE,
])],
'mode' => ['required', 'in:' . implode(',', [
Collection::MODE_MANUAL,
Collection::MODE_SMART,
])],
'sort_mode' => ['nullable', 'in:' . implode(',', [
Collection::SORT_MANUAL,
Collection::SORT_NEWEST,
Collection::SORT_OLDEST,
Collection::SORT_POPULAR,
])],
'smart_rules_json' => ['nullable', 'array'],
'layout_modules_json' => ['nullable', 'array'],
'layout_modules_json.*.key' => ['required_with:layout_modules_json', 'string', 'max:60'],
'layout_modules_json.*.enabled' => ['nullable', 'boolean'],
'layout_modules_json.*.slot' => ['nullable', 'string', 'max:20'],
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator): void {
$type = (string) ($this->input('type') ?: Collection::TYPE_PERSONAL);
if ($type === Collection::TYPE_EDITORIAL && ! $this->user()?->hasRole('admin')) {
$validator->errors()->add('type', 'Only staff can create editorial collections.');
}
if ($type === Collection::TYPE_EDITORIAL && (string) $this->input('editorial_owner_mode') === Collection::EDITORIAL_OWNER_STAFF_ACCOUNT) {
$username = trim((string) $this->input('editorial_owner_username', ''));
if ($username === '') {
$validator->errors()->add('editorial_owner_username', 'Choose the staff account that should own this editorial collection.');
} else {
$target = User::query()->whereRaw('LOWER(username) = ?', [Str::lower($username)])->first();
if (! $target || ! ($target->isAdmin() || $target->isModerator())) {
$validator->errors()->add('editorial_owner_username', 'The editorial owner must be an admin or moderator account.');
}
}
}
if ($this->filled('unpublished_at') && ! $this->filled('published_at')) {
$validator->errors()->add('published_at', 'Set a publish time before adding an unpublish time.');
}
if ((string) $this->input('mode') !== Collection::MODE_SMART) {
return;
}
if (! is_array($this->input('smart_rules_json'))) {
$validator->errors()->add('smart_rules_json', 'Smart collections require at least one valid rule.');
}
});
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Collection;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCollectionCampaignRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
foreach ([
'event_key',
'event_label',
'season_key',
'banner_text',
'badge_label',
'campaign_key',
'campaign_label',
'promotion_tier',
'sponsorship_label',
'partner_label',
'monetization_ready_status',
'brand_safe_status',
'editorial_notes',
'staff_commercial_notes',
] as $field) {
if ($this->has($field) && trim((string) $this->input($field)) === '') {
$this->merge([$field => null]);
}
}
}
public function rules(): array
{
return [
'event_key' => ['nullable', 'string', 'max:80'],
'event_label' => ['nullable', 'string', 'max:120'],
'season_key' => ['nullable', 'string', 'max:80'],
'banner_text' => ['nullable', 'string', 'max:200'],
'badge_label' => ['nullable', 'string', 'max:80'],
'spotlight_style' => ['nullable', 'in:' . implode(',', [
Collection::SPOTLIGHT_STYLE_DEFAULT,
Collection::SPOTLIGHT_STYLE_EDITORIAL,
Collection::SPOTLIGHT_STYLE_SEASONAL,
Collection::SPOTLIGHT_STYLE_CHALLENGE,
Collection::SPOTLIGHT_STYLE_COMMUNITY,
])],
'campaign_key' => ['nullable', 'string', 'max:80'],
'campaign_label' => ['nullable', 'string', 'max:120'],
'commercial_eligibility' => ['nullable', 'boolean'],
'promotion_tier' => ['nullable', 'string', 'max:40'],
'sponsorship_label' => ['nullable', 'string', 'max:120'],
'partner_label' => ['nullable', 'string', 'max:120'],
'monetization_ready_status' => ['nullable', 'string', 'max:40'],
'brand_safe_status' => ['nullable', 'string', 'max:40'],
'editorial_notes' => ['nullable', 'string', 'max:2000'],
'staff_commercial_notes' => ['nullable', 'string', 'max:2000'],
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator): void {
if ($this->filled('staff_commercial_notes') && ! $this->user()?->isAdmin()) {
$validator->errors()->add('staff_commercial_notes', 'Only admins can update staff commercial notes.');
}
});
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Services\CollectionLinkService;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateCollectionEntityLinksRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'entity_links' => ['nullable', 'array', 'max:18'],
'entity_links.*.linked_type' => ['required', 'string', Rule::in(CollectionLinkService::supportedTypes())],
'entity_links.*.linked_id' => ['required', 'integer', 'min:1'],
'entity_links.*.relationship_type' => ['nullable', 'string', 'max:80'],
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Collection;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCollectionLifecycleRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
foreach (['published_at', 'unpublished_at', 'archived_at', 'expired_at'] as $field) {
if ($this->has($field) && trim((string) $this->input($field)) === '') {
$this->merge([$field => null]);
}
}
}
public function rules(): array
{
return [
'lifecycle_state' => ['nullable', 'in:' . implode(',', [
Collection::LIFECYCLE_DRAFT,
Collection::LIFECYCLE_SCHEDULED,
Collection::LIFECYCLE_PUBLISHED,
Collection::LIFECYCLE_FEATURED,
Collection::LIFECYCLE_ARCHIVED,
Collection::LIFECYCLE_EXPIRED,
])],
'visibility' => ['nullable', 'in:' . implode(',', [
Collection::VISIBILITY_PUBLIC,
Collection::VISIBILITY_UNLISTED,
Collection::VISIBILITY_PRIVATE,
])],
'published_at' => ['nullable', 'date'],
'unpublished_at' => ['nullable', 'date', 'after:published_at'],
'archived_at' => ['nullable', 'date'],
'expired_at' => ['nullable', 'date'],
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator): void {
if ($this->filled('unpublished_at')) {
$collection = $this->route('collection');
if (! $this->filled('published_at') && ! optional($collection)->published_at) {
$validator->errors()->add('published_at', 'Set a publish time before adding an unpublish time.');
}
}
});
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCollectionLinkedCollectionsRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'related_collection_ids' => ['nullable', 'array', 'max:12'],
'related_collection_ids.*' => ['required', 'integer', 'distinct'],
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Collection;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCollectionPresentationRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
foreach (['subtitle', 'summary', 'theme_token'] as $field) {
if ($this->has($field) && trim((string) $this->input($field)) === '') {
$this->merge([$field => null]);
}
}
}
public function rules(): array
{
return [
'subtitle' => ['nullable', 'string', 'max:160'],
'summary' => ['nullable', 'string', 'max:320'],
'presentation_style' => ['nullable', 'in:' . implode(',', [
Collection::PRESENTATION_STANDARD,
Collection::PRESENTATION_EDITORIAL_GRID,
Collection::PRESENTATION_HERO_GRID,
Collection::PRESENTATION_MASONRY,
])],
'emphasis_mode' => ['nullable', 'in:' . implode(',', [
Collection::EMPHASIS_COVER_HEAVY,
Collection::EMPHASIS_BALANCED,
Collection::EMPHASIS_ARTWORK_FIRST,
])],
'theme_token' => ['nullable', 'in:default,subtle-blue,violet,amber'],
'layout_modules_json' => ['nullable', 'array'],
'layout_modules_json.*.key' => ['required_with:layout_modules_json', 'string', 'max:60'],
'layout_modules_json.*.enabled' => ['nullable', 'boolean'],
'layout_modules_json.*.slot' => ['nullable', 'string', 'max:20'],
];
}
}

View File

@@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Artwork;
use App\Models\Collection;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class UpdateCollectionRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
$title = (string) $this->input('title', '');
$slug = (string) $this->input('slug', '');
/** @var \App\Models\Collection|null $collection */
$collection = $this->route('collection');
$mode = (string) ($this->input('mode') ?: ($collection?->mode ?? Collection::MODE_MANUAL));
$defaultSortMode = $collection?->sort_mode ?? Collection::SORT_MANUAL;
$sortMode = (string) ($this->input('sort_mode') ?: ($mode === Collection::MODE_SMART ? ($defaultSortMode ?: Collection::SORT_NEWEST) : $defaultSortMode));
if ($slug === '' && $title !== '') {
$slug = Str::slug(Str::limit($title, 140, ''));
}
$this->merge([
'slug' => $slug,
'mode' => $mode,
'sort_mode' => $sortMode,
]);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:2', 'max:120'],
'slug' => ['required', 'string', 'min:2', 'max:140', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
'type' => ['nullable', 'in:' . implode(',', [
Collection::TYPE_PERSONAL,
Collection::TYPE_COMMUNITY,
Collection::TYPE_EDITORIAL,
])],
'editorial_owner_mode' => ['nullable', 'in:' . implode(',', [
Collection::EDITORIAL_OWNER_CREATOR,
Collection::EDITORIAL_OWNER_STAFF_ACCOUNT,
Collection::EDITORIAL_OWNER_SYSTEM,
])],
'editorial_owner_username' => ['nullable', 'string', 'max:60'],
'editorial_owner_label' => ['nullable', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:1000'],
'subtitle' => ['nullable', 'string', 'max:160'],
'summary' => ['nullable', 'string', 'max:320'],
'lifecycle_state' => ['nullable', 'in:' . implode(',', [
Collection::LIFECYCLE_DRAFT,
Collection::LIFECYCLE_SCHEDULED,
Collection::LIFECYCLE_PUBLISHED,
Collection::LIFECYCLE_FEATURED,
Collection::LIFECYCLE_ARCHIVED,
Collection::LIFECYCLE_EXPIRED,
])],
'collaboration_mode' => ['nullable', 'in:' . implode(',', [
Collection::COLLABORATION_CLOSED,
Collection::COLLABORATION_INVITE_ONLY,
Collection::COLLABORATION_OPEN,
])],
'allow_submissions' => ['nullable', 'boolean'],
'allow_comments' => ['nullable', 'boolean'],
'allow_saves' => ['nullable', 'boolean'],
'event_key' => ['nullable', 'string', 'max:80'],
'event_label' => ['nullable', 'string', 'max:120'],
'season_key' => ['nullable', 'string', 'max:80'],
'banner_text' => ['nullable', 'string', 'max:200'],
'badge_label' => ['nullable', 'string', 'max:80'],
'spotlight_style' => ['nullable', 'in:' . implode(',', [
Collection::SPOTLIGHT_STYLE_DEFAULT,
Collection::SPOTLIGHT_STYLE_EDITORIAL,
Collection::SPOTLIGHT_STYLE_SEASONAL,
Collection::SPOTLIGHT_STYLE_CHALLENGE,
Collection::SPOTLIGHT_STYLE_COMMUNITY,
])],
'analytics_enabled' => ['nullable', 'boolean'],
'presentation_style' => ['nullable', 'in:' . implode(',', [
Collection::PRESENTATION_STANDARD,
Collection::PRESENTATION_EDITORIAL_GRID,
Collection::PRESENTATION_HERO_GRID,
Collection::PRESENTATION_MASONRY,
])],
'emphasis_mode' => ['nullable', 'in:' . implode(',', [
Collection::EMPHASIS_COVER_HEAVY,
Collection::EMPHASIS_BALANCED,
Collection::EMPHASIS_ARTWORK_FIRST,
])],
'theme_token' => ['nullable', 'in:default,subtle-blue,violet,amber'],
'series_key' => ['nullable', 'string', 'max:80'],
'series_title' => ['nullable', 'string', 'max:160'],
'series_description' => ['nullable', 'string', 'max:400'],
'series_order' => ['nullable', 'integer', 'min:1', 'max:9999'],
'campaign_key' => ['nullable', 'string', 'max:80'],
'campaign_label' => ['nullable', 'string', 'max:120'],
'commercial_eligibility' => ['nullable', 'boolean'],
'promotion_tier' => ['nullable', 'string', 'max:40'],
'sponsorship_label' => ['nullable', 'string', 'max:120'],
'partner_label' => ['nullable', 'string', 'max:120'],
'monetization_ready_status' => ['nullable', 'string', 'max:40'],
'brand_safe_status' => ['nullable', 'string', 'max:40'],
'published_at' => ['nullable', 'date'],
'unpublished_at' => ['nullable', 'date', 'after:published_at'],
'archived_at' => ['nullable', 'date'],
'expired_at' => ['nullable', 'date'],
'visibility' => ['required', 'in:' . implode(',', [
Collection::VISIBILITY_PUBLIC,
Collection::VISIBILITY_UNLISTED,
Collection::VISIBILITY_PRIVATE,
])],
'mode' => ['required', 'in:' . implode(',', [
Collection::MODE_MANUAL,
Collection::MODE_SMART,
])],
'sort_mode' => ['nullable', 'in:' . implode(',', [
Collection::SORT_MANUAL,
Collection::SORT_NEWEST,
Collection::SORT_OLDEST,
Collection::SORT_POPULAR,
])],
'cover_artwork_id' => ['nullable', 'integer'],
'smart_rules_json' => ['nullable', 'array'],
'layout_modules_json' => ['nullable', 'array'],
'layout_modules_json.*.key' => ['required_with:layout_modules_json', 'string', 'max:60'],
'layout_modules_json.*.enabled' => ['nullable', 'boolean'],
'layout_modules_json.*.slot' => ['nullable', 'string', 'max:20'],
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator): void {
$type = (string) ($this->input('type') ?: Collection::TYPE_PERSONAL);
if ($type === Collection::TYPE_EDITORIAL && ! $this->user()?->hasRole('admin')) {
$validator->errors()->add('type', 'Only staff can edit collections into editorial collections.');
}
if ($type === Collection::TYPE_EDITORIAL && (string) $this->input('editorial_owner_mode') === Collection::EDITORIAL_OWNER_STAFF_ACCOUNT) {
$username = trim((string) $this->input('editorial_owner_username', ''));
if ($username === '') {
$validator->errors()->add('editorial_owner_username', 'Choose the staff account that should own this editorial collection.');
} else {
$target = User::query()->whereRaw('LOWER(username) = ?', [Str::lower($username)])->first();
if (! $target || ! ($target->isAdmin() || $target->isModerator())) {
$validator->errors()->add('editorial_owner_username', 'The editorial owner must be an admin or moderator account.');
}
}
}
if ($this->filled('unpublished_at') && ! $this->filled('published_at') && ! optional($this->route('collection'))->published_at) {
$validator->errors()->add('published_at', 'Set a publish time before adding an unpublish time.');
}
if ((string) $this->input('mode') !== Collection::MODE_SMART) {
return;
}
if (! is_array($this->input('smart_rules_json'))) {
$validator->errors()->add('smart_rules_json', 'Smart collections require at least one valid rule.');
}
});
}
public function passedValidation(): void
{
if (($this->input('mode') ?? Collection::MODE_MANUAL) === Collection::MODE_SMART) {
return;
}
$coverArtworkId = $this->integer('cover_artwork_id');
if (! $coverArtworkId) {
return;
}
/** @var \App\Models\Collection|null $collection */
$collection = $this->route('collection');
$userId = $this->user()?->id;
if (! $collection || ! $userId) {
return;
}
$belongsToUser = Artwork::query()
->where('id', $coverArtworkId)
->where('user_id', $userId)
->whereNull('deleted_at')
->exists();
$isAttached = DB::table('collection_artwork')
->where('collection_id', $collection->id)
->where('artwork_id', $coverArtworkId)
->exists();
if (! $belongsToUser || ! $isAttached) {
throw ValidationException::withMessages([
'cover_artwork_id' => 'Choose a cover artwork that is already attached to this collection.',
]);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCollectionSeriesRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
foreach (['series_key', 'series_title', 'series_description'] as $field) {
if ($this->has($field) && trim((string) $this->input($field)) === '') {
$this->merge([$field => null]);
}
}
if ($this->has('series_order') && trim((string) $this->input('series_order')) === '') {
$this->merge(['series_order' => null]);
}
}
public function rules(): array
{
return [
'series_key' => ['nullable', 'string', 'max:80'],
'series_title' => ['nullable', 'string', 'max:160'],
'series_description' => ['nullable', 'string', 'max:400'],
'series_order' => ['nullable', 'integer', 'min:1', 'max:9999'],
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator): void {
$seriesKey = $this->input('series_key');
$seriesTitle = $this->input('series_title');
$seriesDescription = $this->input('series_description');
$seriesOrder = $this->input('series_order');
$hasSeriesMetadata = filled($seriesTitle) || filled($seriesDescription) || filled($seriesOrder);
if ($hasSeriesMetadata && blank($seriesKey)) {
$validator->errors()->add('series_key', 'Series key is required when series metadata is provided.');
}
});
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Collections;
use App\Models\Collection;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCollectionWorkflowRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'workflow_state' => ['nullable', 'string', 'in:' . implode(',', [
Collection::WORKFLOW_DRAFT,
Collection::WORKFLOW_IN_REVIEW,
Collection::WORKFLOW_APPROVED,
Collection::WORKFLOW_PROGRAMMED,
Collection::WORKFLOW_ARCHIVED,
])],
'program_key' => ['nullable', 'string', 'max:80'],
'partner_key' => ['nullable', 'string', 'max:80'],
'experiment_key' => ['nullable', 'string', 'max:80'],
'placement_eligibility' => ['nullable', 'boolean'],
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\NovaCards;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AdminStoreNovaCardAssetPackRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'slug' => ['required', 'string', 'max:140', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
'name' => ['required', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:500'],
'type' => ['required', Rule::in(['asset', 'template'])],
'preview_image' => ['nullable', 'string', 'max:255'],
'manifest_json' => ['nullable', 'array'],
'official' => ['sometimes', 'boolean'],
'active' => ['sometimes', 'boolean'],
'order_num' => ['sometimes', 'integer', 'min:0', 'max:9999'],
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\NovaCards;
use Illuminate\Foundation\Http\FormRequest;
class AdminStoreNovaCardCategoryRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'slug' => ['required', 'string', 'max:120', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
'name' => ['required', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:400'],
'active' => ['sometimes', 'boolean'],
'order_num' => ['sometimes', 'integer', 'min:0', 'max:9999'],
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\NovaCards;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AdminStoreNovaCardChallengeRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'slug' => ['required', 'string', 'max:140', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
'title' => ['required', 'string', 'max:140'],
'description' => ['nullable', 'string', 'max:1000'],
'prompt' => ['nullable', 'string', 'max:1000'],
'rules_json' => ['nullable', 'array'],
'status' => ['required', Rule::in(['draft', 'active', 'completed', 'archived'])],
'official' => ['sometimes', 'boolean'],
'featured' => ['sometimes', 'boolean'],
'winner_card_id' => ['nullable', 'integer', 'exists:nova_cards,id'],
'starts_at' => ['nullable', 'date'],
'ends_at' => ['nullable', 'date', 'after_or_equal:starts_at'],
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\NovaCards;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AdminStoreNovaCardTemplateRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'slug' => ['required', 'string', 'max:120', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
'name' => ['required', 'string', 'max:160'],
'description' => ['nullable', 'string', 'max:400'],
'preview_image' => ['nullable', 'string', 'max:255'],
'config_json' => ['required', 'array'],
'supported_formats' => ['required', 'array', 'min:1'],
'supported_formats.*' => ['string', Rule::in(array_keys((array) config('nova_cards.formats', [])))],
'active' => ['sometimes', 'boolean'],
'official' => ['sometimes', 'boolean'],
'order_num' => ['sometimes', 'integer', 'min:0', 'max:9999'],
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\NovaCards;
use App\Models\NovaCard;
use App\Services\NovaCards\NovaCardPublishModerationService;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AdminUpdateNovaCardRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'status' => ['sometimes', Rule::in([
NovaCard::STATUS_DRAFT,
NovaCard::STATUS_PROCESSING,
NovaCard::STATUS_PUBLISHED,
NovaCard::STATUS_HIDDEN,
NovaCard::STATUS_REJECTED,
])],
'moderation_status' => ['sometimes', Rule::in([
NovaCard::MOD_PENDING,
NovaCard::MOD_APPROVED,
NovaCard::MOD_FLAGGED,
NovaCard::MOD_REJECTED,
])],
'disposition' => ['sometimes', 'nullable', Rule::in(array_keys(NovaCardPublishModerationService::DISPOSITION_LABELS))],
'featured' => ['sometimes', 'boolean'],
'allow_remix' => ['sometimes', 'boolean'],
];
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\NovaCards;
use App\Models\NovaCard;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SaveNovaCardDraftRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
$validation = (array) config('nova_cards.validation', []);
return [
'title' => ['sometimes', 'string', 'min:2', 'max:' . (int) ($validation['title_max'] ?? 120)],
'quote_text' => ['sometimes', 'string', 'min:' . (int) ($validation['quote_min'] ?? 3), 'max:' . (int) ($validation['quote_max'] ?? 420)],
'quote_author' => ['sometimes', 'nullable', 'string', 'max:160'],
'quote_source' => ['sometimes', 'nullable', 'string', 'max:180'],
'description' => ['sometimes', 'nullable', 'string', 'max:' . (int) ($validation['description_max'] ?? 500)],
'format' => ['sometimes', Rule::in(array_keys((array) config('nova_cards.formats', [])))],
'template_id' => ['sometimes', 'nullable', 'integer', 'exists:nova_card_templates,id'],
'category_id' => ['sometimes', 'nullable', 'integer', 'exists:nova_card_categories,id'],
'background_type' => ['sometimes', Rule::in(['gradient', 'upload', 'template', 'solid'])],
'background_image_id' => ['sometimes', 'nullable', 'integer', 'exists:nova_card_backgrounds,id'],
'visibility' => ['sometimes', Rule::in([NovaCard::VISIBILITY_PUBLIC, NovaCard::VISIBILITY_UNLISTED, NovaCard::VISIBILITY_PRIVATE])],
'allow_download' => ['sometimes', 'boolean'],
'allow_remix' => ['sometimes', 'boolean'],
'editor_mode_last_used' => ['sometimes', Rule::in(['quick', 'full'])],
'tags' => ['sometimes', 'array', 'max:' . (int) ($validation['max_tags'] ?? 8)],
'tags.*' => ['string', 'min:2', 'max:32'],
'project_json' => ['sometimes', 'array'],
'project_json.schema_version' => ['sometimes', 'integer', 'min:1', 'max:9'],
'project_json.text_blocks' => ['sometimes', 'array', 'max:' . (int) ($validation['max_text_blocks'] ?? 8)],
'project_json.text_blocks.*.key' => ['sometimes', 'string', 'max:40'],
'project_json.text_blocks.*.type' => ['sometimes', Rule::in(['title', 'quote', 'author', 'source', 'body', 'caption'])],
'project_json.text_blocks.*.text' => ['sometimes', 'nullable', 'string', 'max:' . (int) ($validation['quote_max'] ?? 420)],
'project_json.text_blocks.*.enabled' => ['sometimes', 'boolean'],
'project_json.assets.pack_ids' => ['sometimes', 'array'],
'project_json.assets.pack_ids.*' => ['integer'],
'project_json.assets.template_pack_ids' => ['sometimes', 'array'],
'project_json.assets.template_pack_ids.*' => ['integer'],
'project_json.assets.items' => ['sometimes', 'array', 'max:' . (int) ($validation['max_asset_items'] ?? 12)],
'project_json.assets.items.*.asset_key' => ['sometimes', 'string', 'max:80'],
'project_json.assets.items.*.label' => ['sometimes', 'string', 'max:120'],
'project_json.assets.items.*.glyph' => ['sometimes', 'string', 'max:4'],
'project_json.layout.alignment' => ['sometimes', Rule::in((array) ($validation['allowed_alignments'] ?? ['left', 'center', 'right']))],
'project_json.layout.layout' => ['sometimes', Rule::in((array) ($validation['allowed_layouts'] ?? []))],
'project_json.layout.position' => ['sometimes', Rule::in((array) ($validation['allowed_positions'] ?? []))],
'project_json.layout.padding' => ['sometimes', Rule::in((array) ($validation['allowed_padding_presets'] ?? []))],
'project_json.layout.max_width' => ['sometimes', Rule::in((array) ($validation['allowed_max_widths'] ?? []))],
'project_json.typography.font_preset' => ['sometimes', Rule::in(array_keys((array) config('nova_cards.font_presets', [])))],
'project_json.typography.quote_size' => ['sometimes', 'integer', 'min:24', 'max:160'],
'project_json.typography.author_size' => ['sometimes', 'integer', 'min:12', 'max:72'],
'project_json.typography.letter_spacing' => ['sometimes', 'integer', 'min:-2', 'max:12'],
'project_json.typography.line_height' => ['sometimes', 'numeric', 'min:0.9', 'max:1.8'],
'project_json.typography.shadow_preset' => ['sometimes', Rule::in(array_column((array) config('nova_cards.shadow_presets', []), 'key'))],
'project_json.typography.text_color' => ['sometimes', 'regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
'project_json.typography.accent_color' => ['sometimes', 'regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
'project_json.background.type' => ['sometimes', Rule::in(['gradient', 'upload', 'template', 'solid'])],
'project_json.background.gradient_preset' => ['sometimes', Rule::in(array_keys((array) config('nova_cards.gradient_presets', [])))],
'project_json.background.gradient_colors' => ['sometimes', 'array', 'min:2', 'max:3'],
'project_json.background.gradient_colors.*' => ['regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
'project_json.background.solid_color' => ['sometimes', 'regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
'project_json.background.overlay_style' => ['sometimes', Rule::in((array) ($validation['allowed_overlay_styles'] ?? []))],
'project_json.background.focal_position' => ['sometimes', Rule::in(array_column((array) config('nova_cards.focal_positions', []), 'key'))],
'project_json.background.blur_level' => ['sometimes', Rule::in((array) ($validation['allowed_blur_levels'] ?? []))],
'project_json.background.opacity' => ['sometimes', Rule::in((array) ($validation['allowed_opacity_levels'] ?? []))],
'project_json.source_context.editor_mode' => ['sometimes', Rule::in(['quick', 'full'])],
'project_json.decorations' => ['sometimes', 'array', 'max:' . (int) ($validation['max_decorations'] ?? 6)],
'project_json.decorations.*.key' => ['sometimes', 'string', 'max:40'],
'project_json.decorations.*.glyph' => ['sometimes', 'string', 'max:4'],
'project_json.decorations.*.placement' => ['sometimes', 'string', 'max:24'],
'project_json.decorations.*.x' => ['sometimes', 'numeric', 'min:0', 'max:1920'],
'project_json.decorations.*.y' => ['sometimes', 'numeric', 'min:0', 'max:1920'],
'project_json.decorations.*.size' => ['sometimes', 'integer', 'min:2', 'max:120'],
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\NovaCards;
use App\Models\NovaCard;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreNovaCardDraftRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'title' => ['nullable', 'string', 'min:2', 'max:' . (int) config('nova_cards.validation.title_max', 120)],
'quote_text' => ['nullable', 'string', 'min:' . (int) config('nova_cards.validation.quote_min', 3), 'max:' . (int) config('nova_cards.validation.quote_max', 420)],
'quote_author' => ['nullable', 'string', 'max:160'],
'quote_source' => ['nullable', 'string', 'max:180'],
'description' => ['nullable', 'string', 'max:' . (int) config('nova_cards.validation.description_max', 500)],
'format' => ['nullable', Rule::in(array_keys((array) config('nova_cards.formats', [])))],
'template_id' => ['nullable', 'integer', 'exists:nova_card_templates,id'],
'category_id' => ['nullable', 'integer', 'exists:nova_card_categories,id'],
'background_type' => ['nullable', Rule::in(['gradient', 'upload', 'template', 'solid'])],
'tags' => ['nullable', 'array', 'max:' . (int) config('nova_cards.validation.max_tags', 8)],
'tags.*' => ['string', 'min:2', 'max:32'],
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\NovaCards;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\Rule;
class UploadNovaCardBackgroundRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
$bytes = (int) config('nova_cards.validation.max_background_upload_bytes', 8 * 1024 * 1024);
$maxKilobytes = (int) ceil($bytes / 1024);
return [
'background' => [
'bail',
'required',
'file',
static function (string $attribute, mixed $value, Closure $fail): void {
if (! $value instanceof UploadedFile) {
return;
}
$path = $value->getRealPath() ?: $value->getPathname();
if (! $value->isValid() || ! is_string($path) || trim($path) === '') {
$fail('The ' . $attribute . ' upload is invalid.');
}
},
'image',
'mimes:jpeg,jpg,png,webp',
'max:' . $maxKilobytes,
Rule::dimensions()->minWidth(480)->minHeight(480),
],
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Studio;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
final class ApplyArtworkAiAssistRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'title' => ['sometimes', 'nullable', 'string', 'max:255'],
'title_mode' => ['sometimes', Rule::in(['replace', 'insert'])],
'description' => ['sometimes', 'nullable', 'string', 'max:5000'],
'description_mode' => ['sometimes', Rule::in(['replace', 'append'])],
'tags' => ['sometimes', 'array', 'max:15'],
'tags.*' => ['string', 'max:64'],
'tag_mode' => ['sometimes', Rule::in(['add', 'replace', 'remove'])],
'category_id' => ['sometimes', 'nullable', 'integer', 'exists:categories,id'],
'content_type_id' => ['sometimes', 'nullable', 'integer', 'exists:content_types,id'],
'similar_actions' => ['sometimes', 'array', 'max:10'],
'similar_actions.*.artwork_id' => ['required_with:similar_actions', 'integer'],
'similar_actions.*.state' => ['required_with:similar_actions', Rule::in(['ignored', 'reviewed'])],
];
}
}