547 lines
24 KiB
PHP
547 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Worlds;
|
|
|
|
use App\Http\Resources\ArtworkListResource;
|
|
use App\Models\Artwork;
|
|
use App\Models\User;
|
|
use App\Models\World;
|
|
use App\Models\WorldSubmission;
|
|
use App\Services\Maturity\ArtworkMaturityService;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
final class WorldSubmissionService
|
|
{
|
|
public function __construct(private readonly ArtworkMaturityService $maturity)
|
|
{
|
|
}
|
|
|
|
public function eligibleWorldOptions(?User $viewer = null): array
|
|
{
|
|
return $this->eligibleWorldsQuery()
|
|
->get()
|
|
->map(fn (World $world): array => $this->mapCreatorWorldOption($world, null, true))
|
|
->all();
|
|
}
|
|
|
|
public function artworkSubmissionOptions(Artwork $artwork, User $viewer): array
|
|
{
|
|
$artwork->loadMissing(['worldSubmissions.world', 'worldSubmissions.reviewer']);
|
|
|
|
$existing = $artwork->worldSubmissions
|
|
->filter(fn (WorldSubmission $submission): bool => $submission->world !== null)
|
|
->keyBy(fn (WorldSubmission $submission): int => (int) $submission->world_id);
|
|
|
|
$eligibleWorlds = $this->eligibleWorldsQuery()->get()->keyBy(fn (World $world): int => (int) $world->id);
|
|
$worlds = $eligibleWorlds;
|
|
|
|
$missingWorldIds = $existing->keys()
|
|
->map(fn ($id): int => (int) $id)
|
|
->reject(fn (int $id): bool => $eligibleWorlds->has($id))
|
|
->values();
|
|
|
|
if ($missingWorldIds->isNotEmpty()) {
|
|
World::query()
|
|
->whereIn('id', $missingWorldIds->all())
|
|
->get()
|
|
->each(fn (World $world) => $worlds->put((int) $world->id, $world));
|
|
}
|
|
|
|
return $worlds
|
|
->sortBy([
|
|
fn (World $world): int => $existing->has((int) $world->id) ? 0 : 1,
|
|
fn (World $world): int => $world->starts_at?->getTimestamp() ?? PHP_INT_MAX,
|
|
fn (World $world): string => Str::lower((string) $world->title),
|
|
])
|
|
->values()
|
|
->map(function (World $world) use ($existing): array {
|
|
$submission = $existing->get((int) $world->id);
|
|
|
|
return $this->mapCreatorWorldOption($world, $submission, $this->isEligibleWorld($world));
|
|
})
|
|
->all();
|
|
}
|
|
|
|
public function syncForArtwork(Artwork $artwork, User $actor, array $entries): void
|
|
{
|
|
$artwork->loadMissing('worldSubmissions');
|
|
|
|
$normalizedEntries = collect($entries)
|
|
->map(function (array $entry): ?array {
|
|
$worldId = (int) ($entry['world_id'] ?? 0);
|
|
if ($worldId < 1) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'world_id' => $worldId,
|
|
'note' => Str::limit(trim((string) ($entry['note'] ?? '')), 1000, ''),
|
|
];
|
|
})
|
|
->filter()
|
|
->unique('world_id')
|
|
->values();
|
|
|
|
$existing = $artwork->worldSubmissions->keyBy(fn (WorldSubmission $submission): int => (int) $submission->world_id);
|
|
$selectedWorldIds = $normalizedEntries->pluck('world_id')->map(fn ($id): int => (int) $id)->all();
|
|
$allWorldIds = array_values(array_unique(array_merge($selectedWorldIds, $existing->keys()->map(fn ($id): int => (int) $id)->all())));
|
|
|
|
$worlds = World::query()
|
|
->whereIn('id', $allWorldIds)
|
|
->get()
|
|
->keyBy(fn (World $world): int => (int) $world->id);
|
|
|
|
$errors = [];
|
|
|
|
foreach ($normalizedEntries as $index => $entry) {
|
|
$world = $worlds->get((int) $entry['world_id']);
|
|
$submission = $existing->get((int) $entry['world_id']);
|
|
|
|
if (! $world) {
|
|
$errors["world_submissions.{$index}.world_id"] = 'Selected world no longer exists.';
|
|
continue;
|
|
}
|
|
|
|
if (! $this->isEligibleWorld($world)) {
|
|
$errors["world_submissions.{$index}.world_id"] = 'That world is not currently accepting community submissions.';
|
|
continue;
|
|
}
|
|
|
|
if ($submission && $submission->isBlockingResubmission()) {
|
|
$errors["world_submissions.{$index}.world_id"] = 'This artwork is blocked from that world until a moderator clears the block.';
|
|
continue;
|
|
}
|
|
|
|
if ($submission && (string) $submission->status === WorldSubmission::STATUS_REMOVED && ! (bool) $world->allow_readd_after_removal) {
|
|
$errors["world_submissions.{$index}.world_id"] = 'That world does not allow re-adding removed artworks right now.';
|
|
}
|
|
}
|
|
|
|
if ($errors !== []) {
|
|
throw ValidationException::withMessages($errors);
|
|
}
|
|
|
|
DB::transaction(function () use ($normalizedEntries, $artwork, $actor, $existing, $worlds, $selectedWorldIds): void {
|
|
foreach ($normalizedEntries as $entry) {
|
|
$worldId = (int) $entry['world_id'];
|
|
$submission = $existing->get($worldId);
|
|
$world = $worlds->get($worldId);
|
|
|
|
$note = ($world?->submission_note_enabled ?? true) ? ($entry['note'] !== '' ? $entry['note'] : null) : null;
|
|
$startingStatus = $world?->submissionStartsAsLive()
|
|
? WorldSubmission::STATUS_LIVE
|
|
: WorldSubmission::STATUS_PENDING;
|
|
$reviewedAt = $startingStatus === WorldSubmission::STATUS_LIVE ? now() : null;
|
|
|
|
if ($submission && $submission->isBlockingResubmission()) {
|
|
continue;
|
|
}
|
|
|
|
if ($submission) {
|
|
$payload = [
|
|
'mode_snapshot' => $world?->participation_mode,
|
|
'note' => $note,
|
|
];
|
|
|
|
if ((string) $submission->status === WorldSubmission::STATUS_REMOVED) {
|
|
$payload = array_merge($payload, [
|
|
'status' => $startingStatus,
|
|
'is_featured' => false,
|
|
'reviewer_note' => null,
|
|
'moderation_reason' => null,
|
|
'reviewed_by_user_id' => null,
|
|
'reviewed_at' => $reviewedAt,
|
|
'removed_at' => null,
|
|
'blocked_at' => null,
|
|
'featured_at' => null,
|
|
]);
|
|
}
|
|
|
|
$submission->forceFill($payload)->save();
|
|
|
|
continue;
|
|
}
|
|
|
|
WorldSubmission::query()->create([
|
|
'world_id' => $worldId,
|
|
'artwork_id' => (int) $artwork->id,
|
|
'submitted_by_user_id' => (int) $actor->id,
|
|
'status' => $startingStatus,
|
|
'is_featured' => false,
|
|
'mode_snapshot' => $world?->participation_mode,
|
|
'note' => $note,
|
|
'reviewed_at' => $reviewedAt,
|
|
]);
|
|
}
|
|
|
|
$existing->each(function (WorldSubmission $submission, int $worldId) use ($selectedWorldIds): void {
|
|
if (in_array((string) $submission->status, [WorldSubmission::STATUS_LIVE, WorldSubmission::STATUS_REMOVED, WorldSubmission::STATUS_BLOCKED], true)) {
|
|
return;
|
|
}
|
|
|
|
if (! in_array($worldId, $selectedWorldIds, true)) {
|
|
$submission->delete();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
public function transition(WorldSubmission $submission, User $reviewer, string $status, ?string $reviewerNote = null): WorldSubmission
|
|
{
|
|
$payload = [
|
|
'status' => $status,
|
|
'reviewer_note' => $this->nullableText($reviewerNote),
|
|
'moderation_reason' => $this->nullableText($reviewerNote),
|
|
];
|
|
|
|
if ($status === WorldSubmission::STATUS_PENDING) {
|
|
$payload['reviewer_note'] = null;
|
|
$payload['moderation_reason'] = null;
|
|
$payload['reviewed_by_user_id'] = null;
|
|
$payload['reviewed_at'] = null;
|
|
$payload['removed_at'] = null;
|
|
$payload['blocked_at'] = null;
|
|
} else {
|
|
$payload['reviewed_by_user_id'] = (int) $reviewer->id;
|
|
$payload['reviewed_at'] = now();
|
|
$payload['removed_at'] = $status === WorldSubmission::STATUS_REMOVED ? now() : null;
|
|
$payload['blocked_at'] = $status === WorldSubmission::STATUS_BLOCKED ? now() : null;
|
|
}
|
|
|
|
if ($status !== WorldSubmission::STATUS_LIVE) {
|
|
$payload['is_featured'] = false;
|
|
$payload['featured_at'] = null;
|
|
}
|
|
|
|
$submission->forceFill($payload)->save();
|
|
|
|
return $submission->fresh(['artwork.user.profile', 'artwork.stats', 'artwork.categories', 'submittedBy.profile', 'reviewer.profile']);
|
|
}
|
|
|
|
public function setFeatured(WorldSubmission $submission, User $reviewer, bool $featured, ?string $reviewerNote = null): WorldSubmission
|
|
{
|
|
$payload = [
|
|
'is_featured' => $featured,
|
|
'featured_at' => $featured ? now() : null,
|
|
'reviewed_by_user_id' => (int) $reviewer->id,
|
|
'reviewed_at' => now(),
|
|
];
|
|
|
|
if ($reviewerNote !== null) {
|
|
$payload['reviewer_note'] = $this->nullableText($reviewerNote);
|
|
$payload['moderation_reason'] = $this->nullableText($reviewerNote);
|
|
}
|
|
|
|
if ((string) $submission->status !== WorldSubmission::STATUS_LIVE) {
|
|
$payload['status'] = WorldSubmission::STATUS_LIVE;
|
|
$payload['removed_at'] = null;
|
|
$payload['blocked_at'] = null;
|
|
}
|
|
|
|
$submission->forceFill($payload)->save();
|
|
|
|
return $submission->fresh(['artwork.user.profile', 'artwork.stats', 'artwork.categories', 'submittedBy.profile', 'reviewer.profile']);
|
|
}
|
|
|
|
public function studioReviewQueue(World $world): array
|
|
{
|
|
$world->loadMissing([
|
|
'worldSubmissions.artwork.user.profile',
|
|
'worldSubmissions.artwork.stats',
|
|
'worldSubmissions.artwork.categories',
|
|
'worldSubmissions.submittedBy.profile',
|
|
'worldSubmissions.reviewer.profile',
|
|
]);
|
|
|
|
$items = $world->worldSubmissions
|
|
->sortBy([
|
|
fn (WorldSubmission $submission): int => match ((string) $submission->status) {
|
|
WorldSubmission::STATUS_PENDING => 0,
|
|
WorldSubmission::STATUS_LIVE => $submission->is_featured ? 1 : 2,
|
|
WorldSubmission::STATUS_REMOVED => 3,
|
|
WorldSubmission::STATUS_BLOCKED => 4,
|
|
default => 4,
|
|
},
|
|
fn (WorldSubmission $submission): int => -1 * ($submission->reviewed_at?->getTimestamp() ?? $submission->created_at?->getTimestamp() ?? 0),
|
|
])
|
|
->values();
|
|
|
|
return [
|
|
'counts' => [
|
|
'pending' => $items->where('status', WorldSubmission::STATUS_PENDING)->count(),
|
|
'live' => $items->where('status', WorldSubmission::STATUS_LIVE)->count(),
|
|
'removed' => $items->where('status', WorldSubmission::STATUS_REMOVED)->count(),
|
|
'blocked' => $items->where('status', WorldSubmission::STATUS_BLOCKED)->count(),
|
|
'featured' => $items->where('is_featured', true)->count(),
|
|
],
|
|
'items' => $items->map(fn (WorldSubmission $submission): array => $this->mapStudioSubmission($submission))->all(),
|
|
];
|
|
}
|
|
|
|
public function publicSectionPayload(World $world, ?User $viewer = null): ?array
|
|
{
|
|
if (! $world->community_section_enabled) {
|
|
return null;
|
|
}
|
|
|
|
$query = Artwork::query()
|
|
->select('artworks.*', 'world_submissions.status as world_submission_status', 'world_submissions.is_featured as world_submission_is_featured', 'world_submissions.note as world_submission_note', 'world_submissions.reviewed_at as world_submission_reviewed_at')
|
|
->join('world_submissions', function ($join) use ($world): void {
|
|
$join->on('world_submissions.artwork_id', '=', 'artworks.id')
|
|
->where('world_submissions.world_id', '=', $world->id)
|
|
->where('world_submissions.status', '=', WorldSubmission::STATUS_LIVE);
|
|
})
|
|
->with(['user.profile', 'categories.contentType', 'stats'])
|
|
->catalogVisible();
|
|
|
|
$this->maturity->applyViewerFilter($query, $viewer);
|
|
|
|
$items = $query
|
|
->orderByRaw('CASE WHEN world_submissions.is_featured = 1 THEN 0 ELSE 1 END')
|
|
->orderByDesc('world_submissions.reviewed_at')
|
|
->limit(24)
|
|
->get()
|
|
->map(fn (Artwork $artwork): array => $this->mapPublicSubmissionArtwork($artwork))
|
|
->all();
|
|
|
|
if ($items === []) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'title' => 'Community submissions',
|
|
'description' => 'Artworks submitted by creators and selected for this world outside the editorial curated-relation system.',
|
|
'items' => $items,
|
|
];
|
|
}
|
|
|
|
private function eligibleWorldsQuery(): Builder
|
|
{
|
|
return World::query()
|
|
->published()
|
|
->where('accepts_submissions', true)
|
|
->whereIn('participation_mode', [World::PARTICIPATION_MODE_MANUAL_APPROVAL, World::PARTICIPATION_MODE_AUTO_ADD])
|
|
->where(function (Builder $builder): void {
|
|
$builder->whereNull('submission_starts_at')
|
|
->orWhere('submission_starts_at', '<=', now());
|
|
})
|
|
->where(function (Builder $builder): void {
|
|
$builder->whereNull('submission_ends_at')
|
|
->orWhere('submission_ends_at', '>=', now());
|
|
})
|
|
->orderBy('submission_ends_at')
|
|
->orderBy('starts_at')
|
|
->orderBy('title');
|
|
}
|
|
|
|
private function isEligibleWorld(World $world): bool
|
|
{
|
|
return $world->isAcceptingSubmissions();
|
|
}
|
|
|
|
private function mapCreatorWorldOption(World $world, ?WorldSubmission $submission, bool $eligible): array
|
|
{
|
|
$status = $submission ? (string) $submission->status : null;
|
|
$selected = match ($status) {
|
|
WorldSubmission::STATUS_PENDING,
|
|
WorldSubmission::STATUS_LIVE => true,
|
|
default => false,
|
|
};
|
|
|
|
$locked = match ($status) {
|
|
WorldSubmission::STATUS_BLOCKED => true,
|
|
WorldSubmission::STATUS_PENDING => ! $eligible,
|
|
WorldSubmission::STATUS_REMOVED => ! $eligible || ! (bool) $world->allow_readd_after_removal,
|
|
default => false,
|
|
};
|
|
|
|
$lockedReason = $locked
|
|
? match ($status) {
|
|
WorldSubmission::STATUS_BLOCKED => 'This artwork is blocked from this world until a moderator clears the block.',
|
|
WorldSubmission::STATUS_PENDING => 'This world is no longer accepting submission changes right now.',
|
|
WorldSubmission::STATUS_REMOVED => (bool) $world->allow_readd_after_removal
|
|
? 'This world is not currently open for re-adding removed artworks.'
|
|
: 'Removed artworks cannot be re-added to this world right now.',
|
|
default => 'This world is locked.',
|
|
}
|
|
: null;
|
|
|
|
return [
|
|
'id' => (int) $world->id,
|
|
'title' => (string) $world->title,
|
|
'slug' => (string) $world->slug,
|
|
'tagline' => (string) ($world->tagline ?? ''),
|
|
'summary' => (string) ($world->summary ?? ''),
|
|
'cover_url' => $world->coverUrl(),
|
|
'timeframe_label' => $this->timeframeLabel($world),
|
|
'submission_window_label' => $this->submissionWindowLabel($world),
|
|
'submission_guidelines' => (string) ($world->submission_guidelines ?? ''),
|
|
'participation_mode' => (string) ($world->participation_mode ?: World::PARTICIPATION_MODE_CLOSED),
|
|
'participation_mode_label' => $this->participationModeLabel((string) ($world->participation_mode ?: World::PARTICIPATION_MODE_CLOSED)),
|
|
'submission_note_enabled' => (bool) $world->submission_note_enabled,
|
|
'is_accepting_submissions' => $eligible,
|
|
'selected' => $selected,
|
|
'selection_locked' => $locked,
|
|
'selection_locked_reason' => $lockedReason,
|
|
'note' => (string) ($submission?->note ?? ''),
|
|
'status' => $status,
|
|
'status_label' => $status ? $this->statusLabel($status, (bool) ($submission?->is_featured ?? false)) : null,
|
|
'reviewer_note' => (string) ($submission?->moderation_reason ?: $submission?->reviewer_note ?? ''),
|
|
'is_featured' => (bool) ($submission?->is_featured ?? false),
|
|
'submitted_at' => $submission?->created_at?->toIso8601String(),
|
|
'reviewed_at' => $submission?->reviewed_at?->toIso8601String(),
|
|
'can_resubmit' => $eligible && (bool) $world->allow_readd_after_removal && $status === WorldSubmission::STATUS_REMOVED,
|
|
];
|
|
}
|
|
|
|
private function mapStudioSubmission(WorldSubmission $submission): array
|
|
{
|
|
$artwork = $submission->artwork;
|
|
$views = (int) ($artwork?->stats?->views ?? 0);
|
|
|
|
return [
|
|
'id' => (int) $submission->id,
|
|
'status' => (string) $submission->status,
|
|
'status_label' => $this->statusLabel((string) $submission->status, (bool) $submission->is_featured),
|
|
'is_featured' => (bool) $submission->is_featured,
|
|
'note' => (string) ($submission->note ?? ''),
|
|
'reviewer_note' => (string) ($submission->moderation_reason ?: $submission->reviewer_note ?? ''),
|
|
'submitted_at' => $submission->created_at?->toIso8601String(),
|
|
'reviewed_at' => $submission->reviewed_at?->toIso8601String(),
|
|
'removed_at' => $submission->removed_at?->toIso8601String(),
|
|
'blocked_at' => $submission->blocked_at?->toIso8601String(),
|
|
'featured_at' => $submission->featured_at?->toIso8601String(),
|
|
'submitted_by' => $submission->submittedBy ? [
|
|
'id' => (int) $submission->submittedBy->id,
|
|
'name' => (string) ($submission->submittedBy->name ?: $submission->submittedBy->username ?: 'Unknown creator'),
|
|
'username' => (string) ($submission->submittedBy->username ?? ''),
|
|
] : null,
|
|
'reviewed_by' => $submission->reviewer ? [
|
|
'id' => (int) $submission->reviewer->id,
|
|
'name' => (string) ($submission->reviewer->name ?: $submission->reviewer->username ?: 'Moderator'),
|
|
] : null,
|
|
'artwork' => $artwork ? [
|
|
'id' => (int) $artwork->id,
|
|
'title' => (string) ($artwork->title ?: 'Untitled artwork'),
|
|
'slug' => (string) ($artwork->slug ?? ''),
|
|
'url' => route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)]),
|
|
'edit_url' => route('studio.artworks.edit', ['id' => $artwork->id]),
|
|
'thumbnail_url' => $artwork->thumbUrl('md'),
|
|
'creator_name' => (string) ($artwork->user?->name ?: $artwork->user?->username ?: ''),
|
|
'meta' => array_values(array_filter([
|
|
$artwork->categories->first()?->name,
|
|
$views > 0 ? number_format($views) . ' views' : null,
|
|
$artwork->visibility ? Str::headline((string) $artwork->visibility) : null,
|
|
])),
|
|
] : null,
|
|
'actions' => [
|
|
'approve' => route('studio.worlds.submissions.approve', ['world' => $submission->world_id, 'submission' => $submission->id]),
|
|
'remove' => route('studio.worlds.submissions.remove', ['world' => $submission->world_id, 'submission' => $submission->id]),
|
|
'block' => route('studio.worlds.submissions.block', ['world' => $submission->world_id, 'submission' => $submission->id]),
|
|
'unblock' => route('studio.worlds.submissions.unblock', ['world' => $submission->world_id, 'submission' => $submission->id]),
|
|
'restore' => route('studio.worlds.submissions.restore', ['world' => $submission->world_id, 'submission' => $submission->id]),
|
|
'feature' => route('studio.worlds.submissions.feature', ['world' => $submission->world_id, 'submission' => $submission->id]),
|
|
'unfeature' => route('studio.worlds.submissions.unfeature', ['world' => $submission->world_id, 'submission' => $submission->id]),
|
|
'pending' => route('studio.worlds.submissions.pending', ['world' => $submission->world_id, 'submission' => $submission->id]),
|
|
],
|
|
];
|
|
}
|
|
|
|
private function mapPublicSubmissionArtwork(Artwork $artwork): array
|
|
{
|
|
$resource = ArtworkListResource::make($artwork)->toArray(request());
|
|
$views = (int) ($artwork->stats?->views ?? 0);
|
|
$status = (string) ($artwork->world_submission_status ?? WorldSubmission::STATUS_LIVE);
|
|
$isFeatured = (bool) ($artwork->world_submission_is_featured ?? false);
|
|
|
|
return [
|
|
'id' => (int) $artwork->id,
|
|
'title' => (string) ($resource['title'] ?? $artwork->title ?? 'Untitled artwork'),
|
|
'subtitle' => (string) ($resource['author']['name'] ?? ''),
|
|
'description' => Str::limit(trim(strip_tags((string) ($artwork->description ?? ''))), 120),
|
|
'url' => (string) ($resource['urls']['canonical'] ?? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)])),
|
|
'image' => $resource['thumbnail_url'] ?? $artwork->thumbUrl('md'),
|
|
'status' => $status,
|
|
'status_label' => $this->statusLabel($status, $isFeatured),
|
|
'context_label' => $isFeatured ? 'Community featured' : 'Community submission',
|
|
'meta' => array_values(array_filter([
|
|
$resource['category']['name'] ?? null,
|
|
$views > 0 ? number_format($views) . ' views' : null,
|
|
])),
|
|
];
|
|
}
|
|
|
|
private function statusLabel(string $status, bool $isFeatured = false): string
|
|
{
|
|
if ($status === WorldSubmission::STATUS_LIVE && $isFeatured) {
|
|
return 'Featured';
|
|
}
|
|
|
|
return match ($status) {
|
|
WorldSubmission::STATUS_PENDING => 'Pending',
|
|
WorldSubmission::STATUS_LIVE => 'Live',
|
|
WorldSubmission::STATUS_REMOVED => 'Removed',
|
|
WorldSubmission::STATUS_BLOCKED => 'Blocked',
|
|
default => Str::headline($status),
|
|
};
|
|
}
|
|
|
|
private function participationModeLabel(string $mode): string
|
|
{
|
|
return match ($mode) {
|
|
World::PARTICIPATION_MODE_MANUAL_APPROVAL => 'Manual approval',
|
|
World::PARTICIPATION_MODE_AUTO_ADD => 'Auto add',
|
|
World::PARTICIPATION_MODE_CLOSED => 'Closed',
|
|
default => Str::headline($mode),
|
|
};
|
|
}
|
|
|
|
private function timeframeLabel(World $world): string
|
|
{
|
|
if ($world->starts_at && $world->ends_at) {
|
|
return $world->starts_at->format('M j') . ' - ' . $world->ends_at->format('M j, Y');
|
|
}
|
|
|
|
if ($world->starts_at) {
|
|
return 'Starts ' . $world->starts_at->format('M j, Y');
|
|
}
|
|
|
|
if ($world->ends_at) {
|
|
return 'Until ' . $world->ends_at->format('M j, Y');
|
|
}
|
|
|
|
return 'Open-ended world';
|
|
}
|
|
|
|
private function submissionWindowLabel(World $world): string
|
|
{
|
|
$start = $world->submission_starts_at;
|
|
$end = $world->submission_ends_at;
|
|
|
|
if ($start && $end) {
|
|
return $start->format('M j') . ' - ' . $end->format('M j, Y');
|
|
}
|
|
|
|
if ($start) {
|
|
return 'Opens ' . $start->format('M j, Y');
|
|
}
|
|
|
|
if ($end) {
|
|
return 'Open until ' . $end->format('M j, Y');
|
|
}
|
|
|
|
return 'Open submissions';
|
|
}
|
|
|
|
private function nullableText(?string $value): ?string
|
|
{
|
|
$value = trim((string) $value);
|
|
|
|
return $value !== '' ? $value : null;
|
|
}
|
|
} |