Implement creator studio and upload updates
This commit is contained in:
280
app/Services/Studio/Providers/ArtworkStudioProvider.php
Normal file
280
app/Services/Studio/Providers/ArtworkStudioProvider.php
Normal file
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Studio\Providers;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkStats;
|
||||
use App\Models\User;
|
||||
use App\Services\Studio\Contracts\CreatorStudioProvider;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class ArtworkStudioProvider implements CreatorStudioProvider
|
||||
{
|
||||
public function key(): string
|
||||
{
|
||||
return 'artworks';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Artworks';
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return 'fa-solid fa-images';
|
||||
}
|
||||
|
||||
public function createUrl(): string
|
||||
{
|
||||
return '/upload';
|
||||
}
|
||||
|
||||
public function indexUrl(): string
|
||||
{
|
||||
return route('studio.artworks');
|
||||
}
|
||||
|
||||
public function summary(User $user): array
|
||||
{
|
||||
$baseQuery = Artwork::query()->withTrashed()->where('user_id', $user->id);
|
||||
|
||||
$count = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
$draftCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->where(function (Builder $query): void {
|
||||
$query->where('is_public', false)
|
||||
->orWhere('artwork_status', 'draft');
|
||||
})
|
||||
->count();
|
||||
|
||||
$publishedCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->where('is_public', true)
|
||||
->whereNotNull('published_at')
|
||||
->count();
|
||||
|
||||
$recentPublishedCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->where('is_public', true)
|
||||
->where('published_at', '>=', now()->subDays(30))
|
||||
->count();
|
||||
|
||||
$archivedCount = Artwork::onlyTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->count();
|
||||
|
||||
return [
|
||||
'key' => $this->key(),
|
||||
'label' => $this->label(),
|
||||
'icon' => $this->icon(),
|
||||
'count' => $count,
|
||||
'draft_count' => $draftCount,
|
||||
'published_count' => $publishedCount,
|
||||
'archived_count' => $archivedCount,
|
||||
'trend_value' => $recentPublishedCount,
|
||||
'trend_label' => 'published in 30d',
|
||||
'quick_action_label' => 'Upload artwork',
|
||||
'index_url' => $this->indexUrl(),
|
||||
'create_url' => $this->createUrl(),
|
||||
];
|
||||
}
|
||||
|
||||
public function items(User $user, string $bucket = 'all', int $limit = 200): Collection
|
||||
{
|
||||
$query = Artwork::query()
|
||||
->withTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->with(['stats', 'categories', 'tags'])
|
||||
->orderByDesc('updated_at')
|
||||
->limit($limit);
|
||||
|
||||
if ($bucket === 'drafts') {
|
||||
$query->whereNull('deleted_at')
|
||||
->where(function (Builder $builder): void {
|
||||
$builder->where('is_public', false)
|
||||
->orWhere('artwork_status', 'draft');
|
||||
});
|
||||
} elseif ($bucket === 'scheduled') {
|
||||
$query->whereNull('deleted_at')
|
||||
->where('artwork_status', 'scheduled');
|
||||
} elseif ($bucket === 'archived') {
|
||||
$query->onlyTrashed();
|
||||
} elseif ($bucket === 'published') {
|
||||
$query->whereNull('deleted_at')
|
||||
->where('is_public', true)
|
||||
->whereNotNull('published_at');
|
||||
} else {
|
||||
$query->whereNull('deleted_at');
|
||||
}
|
||||
|
||||
return $query->get()->map(fn (Artwork $artwork): array => $this->mapItem($artwork));
|
||||
}
|
||||
|
||||
public function topItems(User $user, int $limit = 5): Collection
|
||||
{
|
||||
return Artwork::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('deleted_at')
|
||||
->where('is_public', true)
|
||||
->with(['stats', 'categories', 'tags'])
|
||||
->whereHas('stats')
|
||||
->orderByDesc(
|
||||
ArtworkStats::select('ranking_score')
|
||||
->whereColumn('artwork_stats.artwork_id', 'artworks.id')
|
||||
->limit(1)
|
||||
)
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn (Artwork $artwork): array => $this->mapItem($artwork));
|
||||
}
|
||||
|
||||
public function analytics(User $user): array
|
||||
{
|
||||
$totals = DB::table('artwork_stats')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_stats.artwork_id')
|
||||
->where('artworks.user_id', $user->id)
|
||||
->whereNull('artworks.deleted_at')
|
||||
->selectRaw('COALESCE(SUM(artwork_stats.views), 0) as views')
|
||||
->selectRaw('COALESCE(SUM(artwork_stats.favorites), 0) as appreciation')
|
||||
->selectRaw('COALESCE(SUM(artwork_stats.shares_count), 0) as shares')
|
||||
->selectRaw('COALESCE(SUM(artwork_stats.comments_count), 0) as comments')
|
||||
->selectRaw('COALESCE(SUM(artwork_stats.downloads), 0) as saves')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'views' => (int) ($totals->views ?? 0),
|
||||
'appreciation' => (int) ($totals->appreciation ?? 0),
|
||||
'shares' => (int) ($totals->shares ?? 0),
|
||||
'comments' => (int) ($totals->comments ?? 0),
|
||||
'saves' => (int) ($totals->saves ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function scheduledItems(User $user, int $limit = 50): Collection
|
||||
{
|
||||
return $this->items($user, 'scheduled', $limit);
|
||||
}
|
||||
|
||||
private function mapItem(Artwork $artwork): array
|
||||
{
|
||||
$stats = $artwork->stats;
|
||||
$status = $artwork->deleted_at
|
||||
? 'archived'
|
||||
: ($artwork->artwork_status === 'scheduled'
|
||||
? 'scheduled'
|
||||
: ((bool) $artwork->is_public ? 'published' : 'draft'));
|
||||
|
||||
$category = $artwork->categories->first();
|
||||
$visibility = $artwork->visibility ?: ((bool) $artwork->is_public ? Artwork::VISIBILITY_PUBLIC : Artwork::VISIBILITY_PRIVATE);
|
||||
|
||||
return [
|
||||
'id' => sprintf('%s:%d', $this->key(), (int) $artwork->id),
|
||||
'numeric_id' => (int) $artwork->id,
|
||||
'module' => $this->key(),
|
||||
'module_label' => $this->label(),
|
||||
'module_icon' => $this->icon(),
|
||||
'title' => $artwork->title,
|
||||
'subtitle' => $category?->name,
|
||||
'description' => $artwork->description,
|
||||
'status' => $status,
|
||||
'visibility' => $visibility,
|
||||
'image_url' => $artwork->thumbUrl('md'),
|
||||
'preview_url' => $artwork->published_at ? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]) : route('studio.artworks.edit', ['id' => $artwork->id]),
|
||||
'view_url' => $artwork->published_at ? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]) : route('studio.artworks.edit', ['id' => $artwork->id]),
|
||||
'edit_url' => route('studio.artworks.edit', ['id' => $artwork->id]),
|
||||
'manage_url' => route('studio.artworks.edit', ['id' => $artwork->id]),
|
||||
'analytics_url' => route('studio.artworks.analytics', ['id' => $artwork->id]),
|
||||
'create_url' => $this->createUrl(),
|
||||
'actions' => $this->actionsFor($artwork, $status),
|
||||
'created_at' => $artwork->created_at?->toIso8601String(),
|
||||
'updated_at' => $artwork->updated_at?->toIso8601String(),
|
||||
'published_at' => $artwork->published_at?->toIso8601String(),
|
||||
'scheduled_at' => $artwork->publish_at?->toIso8601String(),
|
||||
'schedule_timezone' => $artwork->artwork_timezone,
|
||||
'featured' => false,
|
||||
'metrics' => [
|
||||
'views' => (int) ($stats?->views ?? 0),
|
||||
'appreciation' => (int) ($stats?->favorites ?? 0),
|
||||
'shares' => (int) ($stats?->shares_count ?? 0),
|
||||
'comments' => (int) ($stats?->comments_count ?? 0),
|
||||
'saves' => (int) ($stats?->downloads ?? 0),
|
||||
],
|
||||
'engagement_score' => (int) ($stats?->views ?? 0)
|
||||
+ ((int) ($stats?->favorites ?? 0) * 2)
|
||||
+ ((int) ($stats?->comments_count ?? 0) * 3)
|
||||
+ ((int) ($stats?->shares_count ?? 0) * 2),
|
||||
'taxonomies' => [
|
||||
'categories' => $artwork->categories->map(fn ($entry): array => [
|
||||
'id' => (int) $entry->id,
|
||||
'name' => (string) $entry->name,
|
||||
'slug' => (string) $entry->slug,
|
||||
])->values()->all(),
|
||||
'tags' => $artwork->tags->map(fn ($entry): array => [
|
||||
'id' => (int) $entry->id,
|
||||
'name' => (string) $entry->name,
|
||||
'slug' => (string) $entry->slug,
|
||||
])->values()->all(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function actionsFor(Artwork $artwork, string $status): array
|
||||
{
|
||||
$actions = [];
|
||||
|
||||
if ($status === 'draft') {
|
||||
$actions[] = $this->requestAction('publish', 'Publish', 'fa-solid fa-rocket', route('api.studio.artworks.toggle', ['id' => $artwork->id]), ['action' => 'publish']);
|
||||
}
|
||||
|
||||
if ($status === 'scheduled') {
|
||||
$actions[] = $this->requestAction('publish_now', 'Publish now', 'fa-solid fa-bolt', route('api.studio.schedule.publishNow', ['module' => 'artworks', 'id' => $artwork->id]), []);
|
||||
$actions[] = $this->requestAction('unschedule', 'Unschedule', 'fa-solid fa-calendar-xmark', route('api.studio.schedule.unschedule', ['module' => 'artworks', 'id' => $artwork->id]), []);
|
||||
}
|
||||
|
||||
if ($status === 'published') {
|
||||
$actions[] = $this->requestAction('unpublish', 'Unpublish', 'fa-solid fa-eye-slash', route('api.studio.artworks.toggle', ['id' => $artwork->id]), ['action' => 'unpublish']);
|
||||
$actions[] = $this->requestAction('archive', 'Archive', 'fa-solid fa-box-archive', route('api.studio.artworks.toggle', ['id' => $artwork->id]), ['action' => 'archive']);
|
||||
}
|
||||
|
||||
if ($status === 'archived') {
|
||||
$actions[] = $this->requestAction('restore', 'Restore', 'fa-solid fa-rotate-left', route('api.studio.artworks.toggle', ['id' => $artwork->id]), ['action' => 'unarchive']);
|
||||
}
|
||||
|
||||
$actions[] = $this->requestAction(
|
||||
'delete',
|
||||
'Delete',
|
||||
'fa-solid fa-trash',
|
||||
route('api.studio.artworks.bulk'),
|
||||
[
|
||||
'action' => 'delete',
|
||||
'artwork_ids' => [$artwork->id],
|
||||
'confirm' => 'DELETE',
|
||||
],
|
||||
'Delete this artwork permanently?'
|
||||
);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
private function requestAction(string $key, string $label, string $icon, string $url, array $payload, ?string $confirm = null): array
|
||||
{
|
||||
return [
|
||||
'key' => $key,
|
||||
'label' => $label,
|
||||
'icon' => $icon,
|
||||
'type' => 'request',
|
||||
'method' => 'post',
|
||||
'url' => $url,
|
||||
'payload' => $payload,
|
||||
'confirm' => $confirm,
|
||||
];
|
||||
}
|
||||
}
|
||||
261
app/Services/Studio/Providers/CardStudioProvider.php
Normal file
261
app/Services/Studio/Providers/CardStudioProvider.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Studio\Providers;
|
||||
|
||||
use App\Models\NovaCard;
|
||||
use App\Models\User;
|
||||
use App\Services\Studio\Contracts\CreatorStudioProvider;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class CardStudioProvider implements CreatorStudioProvider
|
||||
{
|
||||
public function key(): string
|
||||
{
|
||||
return 'cards';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Cards';
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return 'fa-solid fa-id-card';
|
||||
}
|
||||
|
||||
public function createUrl(): string
|
||||
{
|
||||
return route('studio.cards.create');
|
||||
}
|
||||
|
||||
public function indexUrl(): string
|
||||
{
|
||||
return route('studio.cards.index');
|
||||
}
|
||||
|
||||
public function summary(User $user): array
|
||||
{
|
||||
$baseQuery = NovaCard::query()->withTrashed()->where('user_id', $user->id);
|
||||
|
||||
$count = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->whereNotIn('status', [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED])
|
||||
->count();
|
||||
|
||||
$draftCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->where('status', NovaCard::STATUS_DRAFT)
|
||||
->count();
|
||||
|
||||
$publishedCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->where('status', NovaCard::STATUS_PUBLISHED)
|
||||
->count();
|
||||
|
||||
$recentPublishedCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->where('status', NovaCard::STATUS_PUBLISHED)
|
||||
->where('published_at', '>=', now()->subDays(30))
|
||||
->count();
|
||||
|
||||
$archivedCount = (clone $baseQuery)
|
||||
->where(function (Builder $query): void {
|
||||
$query->whereNotNull('deleted_at')
|
||||
->orWhereIn('status', [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED]);
|
||||
})
|
||||
->count();
|
||||
|
||||
return [
|
||||
'key' => $this->key(),
|
||||
'label' => $this->label(),
|
||||
'icon' => $this->icon(),
|
||||
'count' => $count,
|
||||
'draft_count' => $draftCount,
|
||||
'published_count' => $publishedCount,
|
||||
'archived_count' => $archivedCount,
|
||||
'trend_value' => $recentPublishedCount,
|
||||
'trend_label' => 'published in 30d',
|
||||
'quick_action_label' => 'Create card',
|
||||
'index_url' => $this->indexUrl(),
|
||||
'create_url' => $this->createUrl(),
|
||||
];
|
||||
}
|
||||
|
||||
public function items(User $user, string $bucket = 'all', int $limit = 200): Collection
|
||||
{
|
||||
$query = NovaCard::query()
|
||||
->withTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->with(['category', 'tags'])
|
||||
->orderByDesc('updated_at')
|
||||
->limit($limit);
|
||||
|
||||
if ($bucket === 'drafts') {
|
||||
$query->whereNull('deleted_at')->where('status', NovaCard::STATUS_DRAFT);
|
||||
} elseif ($bucket === 'scheduled') {
|
||||
$query->whereNull('deleted_at')->where('status', NovaCard::STATUS_SCHEDULED);
|
||||
} elseif ($bucket === 'archived') {
|
||||
$query->where(function (Builder $builder): void {
|
||||
$builder->whereNotNull('deleted_at')
|
||||
->orWhereIn('status', [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED]);
|
||||
});
|
||||
} elseif ($bucket === 'published') {
|
||||
$query->whereNull('deleted_at')->where('status', NovaCard::STATUS_PUBLISHED);
|
||||
} else {
|
||||
$query->whereNull('deleted_at')->whereNotIn('status', [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED]);
|
||||
}
|
||||
|
||||
return $query->get()->map(fn (NovaCard $card): array => $this->mapItem($card));
|
||||
}
|
||||
|
||||
public function topItems(User $user, int $limit = 5): Collection
|
||||
{
|
||||
return NovaCard::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('deleted_at')
|
||||
->where('status', NovaCard::STATUS_PUBLISHED)
|
||||
->orderByDesc('trending_score')
|
||||
->orderByDesc('views_count')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn (NovaCard $card): array => $this->mapItem($card));
|
||||
}
|
||||
|
||||
public function analytics(User $user): array
|
||||
{
|
||||
$totals = NovaCard::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COALESCE(SUM(views_count), 0) as views')
|
||||
->selectRaw('COALESCE(SUM(likes_count + favorites_count), 0) as appreciation')
|
||||
->selectRaw('COALESCE(SUM(shares_count), 0) as shares')
|
||||
->selectRaw('COALESCE(SUM(comments_count), 0) as comments')
|
||||
->selectRaw('COALESCE(SUM(saves_count), 0) as saves')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'views' => (int) ($totals->views ?? 0),
|
||||
'appreciation' => (int) ($totals->appreciation ?? 0),
|
||||
'shares' => (int) ($totals->shares ?? 0),
|
||||
'comments' => (int) ($totals->comments ?? 0),
|
||||
'saves' => (int) ($totals->saves ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function scheduledItems(User $user, int $limit = 50): Collection
|
||||
{
|
||||
return $this->items($user, 'scheduled', $limit);
|
||||
}
|
||||
|
||||
private function mapItem(NovaCard $card): array
|
||||
{
|
||||
$status = $card->deleted_at || in_array($card->status, [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED], true)
|
||||
? 'archived'
|
||||
: $card->status;
|
||||
|
||||
return [
|
||||
'id' => sprintf('%s:%d', $this->key(), (int) $card->id),
|
||||
'numeric_id' => (int) $card->id,
|
||||
'module' => $this->key(),
|
||||
'module_label' => $this->label(),
|
||||
'module_icon' => $this->icon(),
|
||||
'title' => $card->title,
|
||||
'subtitle' => $card->category?->name ?: strtoupper((string) $card->format),
|
||||
'description' => $card->description,
|
||||
'status' => $status,
|
||||
'visibility' => $card->visibility,
|
||||
'image_url' => $card->previewUrl(),
|
||||
'preview_url' => route('studio.cards.preview', ['id' => $card->id]),
|
||||
'view_url' => $card->status === NovaCard::STATUS_PUBLISHED ? $card->publicUrl() : route('studio.cards.preview', ['id' => $card->id]),
|
||||
'edit_url' => route('studio.cards.edit', ['id' => $card->id]),
|
||||
'manage_url' => route('studio.cards.edit', ['id' => $card->id]),
|
||||
'analytics_url' => route('studio.cards.analytics', ['id' => $card->id]),
|
||||
'create_url' => $this->createUrl(),
|
||||
'actions' => $this->actionsFor($card, $status),
|
||||
'created_at' => $card->created_at?->toIso8601String(),
|
||||
'updated_at' => $card->updated_at?->toIso8601String(),
|
||||
'published_at' => $card->published_at?->toIso8601String(),
|
||||
'scheduled_at' => $card->scheduled_for?->toIso8601String(),
|
||||
'schedule_timezone' => $card->scheduling_timezone,
|
||||
'featured' => (bool) $card->featured,
|
||||
'metrics' => [
|
||||
'views' => (int) $card->views_count,
|
||||
'appreciation' => (int) ($card->likes_count + $card->favorites_count),
|
||||
'shares' => (int) $card->shares_count,
|
||||
'comments' => (int) $card->comments_count,
|
||||
'saves' => (int) $card->saves_count,
|
||||
],
|
||||
'engagement_score' => (int) $card->views_count
|
||||
+ ((int) $card->likes_count * 2)
|
||||
+ ((int) $card->favorites_count * 2)
|
||||
+ ((int) $card->comments_count * 3)
|
||||
+ ((int) $card->shares_count * 2)
|
||||
+ ((int) $card->saves_count * 2),
|
||||
'taxonomies' => [
|
||||
'categories' => $card->category ? [[
|
||||
'id' => (int) $card->category->id,
|
||||
'name' => (string) $card->category->name,
|
||||
'slug' => (string) $card->category->slug,
|
||||
]] : [],
|
||||
'tags' => $card->tags->map(fn ($entry): array => [
|
||||
'id' => (int) $entry->id,
|
||||
'name' => (string) $entry->name,
|
||||
'slug' => (string) $entry->slug,
|
||||
])->values()->all(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function actionsFor(NovaCard $card, string $status): array
|
||||
{
|
||||
$actions = [
|
||||
[
|
||||
'key' => 'duplicate',
|
||||
'label' => 'Duplicate',
|
||||
'icon' => 'fa-solid fa-id-card',
|
||||
'type' => 'request',
|
||||
'method' => 'post',
|
||||
'url' => route('api.cards.duplicate', ['id' => $card->id]),
|
||||
'redirect_pattern' => route('studio.cards.edit', ['id' => '__ID__']),
|
||||
],
|
||||
];
|
||||
|
||||
if ($status === NovaCard::STATUS_DRAFT) {
|
||||
$actions[] = [
|
||||
'key' => 'delete',
|
||||
'label' => 'Delete draft',
|
||||
'icon' => 'fa-solid fa-trash',
|
||||
'type' => 'request',
|
||||
'method' => 'delete',
|
||||
'url' => route('api.cards.drafts.destroy', ['id' => $card->id]),
|
||||
'confirm' => 'Delete this card draft?',
|
||||
];
|
||||
}
|
||||
|
||||
if ($status === NovaCard::STATUS_SCHEDULED) {
|
||||
$actions[] = [
|
||||
'key' => 'publish_now',
|
||||
'label' => 'Publish now',
|
||||
'icon' => 'fa-solid fa-bolt',
|
||||
'type' => 'request',
|
||||
'method' => 'post',
|
||||
'url' => route('api.studio.schedule.publishNow', ['module' => 'cards', 'id' => $card->id]),
|
||||
];
|
||||
$actions[] = [
|
||||
'key' => 'unschedule',
|
||||
'label' => 'Unschedule',
|
||||
'icon' => 'fa-solid fa-calendar-xmark',
|
||||
'type' => 'request',
|
||||
'method' => 'post',
|
||||
'url' => route('api.studio.schedule.unschedule', ['module' => 'cards', 'id' => $card->id]),
|
||||
];
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
}
|
||||
308
app/Services/Studio/Providers/CollectionStudioProvider.php
Normal file
308
app/Services/Studio/Providers/CollectionStudioProvider.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Studio\Providers;
|
||||
|
||||
use App\Models\Collection;
|
||||
use App\Models\User;
|
||||
use App\Services\CollectionService;
|
||||
use App\Services\Studio\Contracts\CreatorStudioProvider;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection as SupportCollection;
|
||||
|
||||
final class CollectionStudioProvider implements CreatorStudioProvider
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionService $collections,
|
||||
) {
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'collections';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Collections';
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return 'fa-solid fa-layer-group';
|
||||
}
|
||||
|
||||
public function createUrl(): string
|
||||
{
|
||||
return route('settings.collections.create');
|
||||
}
|
||||
|
||||
public function indexUrl(): string
|
||||
{
|
||||
return route('studio.collections');
|
||||
}
|
||||
|
||||
public function summary(User $user): array
|
||||
{
|
||||
$baseQuery = Collection::query()->withTrashed()->where('user_id', $user->id);
|
||||
|
||||
$count = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->where('lifecycle_state', '!=', Collection::LIFECYCLE_ARCHIVED)
|
||||
->count();
|
||||
|
||||
$draftCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->where(function (Builder $query): void {
|
||||
$query->where('lifecycle_state', Collection::LIFECYCLE_DRAFT)
|
||||
->orWhere('workflow_state', Collection::WORKFLOW_DRAFT)
|
||||
->orWhere('workflow_state', Collection::WORKFLOW_IN_REVIEW);
|
||||
})
|
||||
->count();
|
||||
|
||||
$publishedCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_SCHEDULED])
|
||||
->count();
|
||||
|
||||
$recentPublishedCount = (clone $baseQuery)
|
||||
->whereNull('deleted_at')
|
||||
->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_SCHEDULED])
|
||||
->where('published_at', '>=', now()->subDays(30))
|
||||
->count();
|
||||
|
||||
$archivedCount = (clone $baseQuery)
|
||||
->where(function (Builder $query): void {
|
||||
$query->whereNotNull('deleted_at')
|
||||
->orWhere('lifecycle_state', Collection::LIFECYCLE_ARCHIVED);
|
||||
})
|
||||
->count();
|
||||
|
||||
return [
|
||||
'key' => $this->key(),
|
||||
'label' => $this->label(),
|
||||
'icon' => $this->icon(),
|
||||
'count' => $count,
|
||||
'draft_count' => $draftCount,
|
||||
'published_count' => $publishedCount,
|
||||
'archived_count' => $archivedCount,
|
||||
'trend_value' => $recentPublishedCount,
|
||||
'trend_label' => 'published in 30d',
|
||||
'quick_action_label' => 'Create collection',
|
||||
'index_url' => $this->indexUrl(),
|
||||
'create_url' => $this->createUrl(),
|
||||
];
|
||||
}
|
||||
|
||||
public function items(User $user, string $bucket = 'all', int $limit = 200): SupportCollection
|
||||
{
|
||||
$query = Collection::query()
|
||||
->withTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->with(['user.profile', 'coverArtwork'])
|
||||
->orderByDesc('updated_at')
|
||||
->limit($limit);
|
||||
|
||||
if ($bucket === 'drafts') {
|
||||
$query->whereNull('deleted_at')
|
||||
->where(function (Builder $builder): void {
|
||||
$builder->where('lifecycle_state', Collection::LIFECYCLE_DRAFT)
|
||||
->orWhere('workflow_state', Collection::WORKFLOW_DRAFT)
|
||||
->orWhere('workflow_state', Collection::WORKFLOW_IN_REVIEW);
|
||||
});
|
||||
} elseif ($bucket === 'scheduled') {
|
||||
$query->whereNull('deleted_at')
|
||||
->where('lifecycle_state', Collection::LIFECYCLE_SCHEDULED);
|
||||
} elseif ($bucket === 'archived') {
|
||||
$query->where(function (Builder $builder): void {
|
||||
$builder->whereNotNull('deleted_at')
|
||||
->orWhere('lifecycle_state', Collection::LIFECYCLE_ARCHIVED);
|
||||
});
|
||||
} elseif ($bucket === 'published') {
|
||||
$query->whereNull('deleted_at')
|
||||
->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_SCHEDULED]);
|
||||
} else {
|
||||
$query->whereNull('deleted_at')->where('lifecycle_state', '!=', Collection::LIFECYCLE_ARCHIVED);
|
||||
}
|
||||
|
||||
return collect($this->collections->mapCollectionCardPayloads($query->get(), true, $user))
|
||||
->map(fn (array $item): array => $this->mapItem($item));
|
||||
}
|
||||
|
||||
public function topItems(User $user, int $limit = 5): SupportCollection
|
||||
{
|
||||
$collections = Collection::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('deleted_at')
|
||||
->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED])
|
||||
->with(['user.profile', 'coverArtwork'])
|
||||
->orderByDesc('ranking_score')
|
||||
->orderByDesc('views_count')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
return collect($this->collections->mapCollectionCardPayloads($collections, true, $user))
|
||||
->map(fn (array $item): array => $this->mapItem($item));
|
||||
}
|
||||
|
||||
public function analytics(User $user): array
|
||||
{
|
||||
$totals = Collection::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COALESCE(SUM(views_count), 0) as views')
|
||||
->selectRaw('COALESCE(SUM(likes_count + followers_count), 0) as appreciation')
|
||||
->selectRaw('COALESCE(SUM(shares_count), 0) as shares')
|
||||
->selectRaw('COALESCE(SUM(comments_count), 0) as comments')
|
||||
->selectRaw('COALESCE(SUM(saves_count), 0) as saves')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'views' => (int) ($totals->views ?? 0),
|
||||
'appreciation' => (int) ($totals->appreciation ?? 0),
|
||||
'shares' => (int) ($totals->shares ?? 0),
|
||||
'comments' => (int) ($totals->comments ?? 0),
|
||||
'saves' => (int) ($totals->saves ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function scheduledItems(User $user, int $limit = 50): SupportCollection
|
||||
{
|
||||
return $this->items($user, 'scheduled', $limit);
|
||||
}
|
||||
|
||||
private function mapItem(array $item): array
|
||||
{
|
||||
$status = $item['lifecycle_state'] ?? 'draft';
|
||||
if ($status === Collection::LIFECYCLE_FEATURED) {
|
||||
$status = 'published';
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => sprintf('%s:%d', $this->key(), (int) $item['id']),
|
||||
'numeric_id' => (int) $item['id'],
|
||||
'module' => $this->key(),
|
||||
'module_label' => $this->label(),
|
||||
'module_icon' => $this->icon(),
|
||||
'title' => $item['title'],
|
||||
'subtitle' => $item['subtitle'] ?: ($item['type'] ? ucfirst((string) $item['type']) : null),
|
||||
'description' => $item['summary'] ?: $item['description'],
|
||||
'status' => $status,
|
||||
'visibility' => $item['visibility'],
|
||||
'image_url' => $item['cover_image'],
|
||||
'preview_url' => $item['url'],
|
||||
'view_url' => $item['url'],
|
||||
'edit_url' => $item['edit_url'] ?: $item['manage_url'],
|
||||
'manage_url' => $item['manage_url'],
|
||||
'analytics_url' => route('settings.collections.analytics', ['collection' => $item['id']]),
|
||||
'create_url' => $this->createUrl(),
|
||||
'actions' => $this->actionsFor($item, $status),
|
||||
'created_at' => $item['published_at'] ?? $item['updated_at'],
|
||||
'updated_at' => $item['updated_at'],
|
||||
'published_at' => $item['published_at'] ?? null,
|
||||
'scheduled_at' => $status === Collection::LIFECYCLE_SCHEDULED ? ($item['published_at'] ?? null) : null,
|
||||
'featured' => (bool) ($item['is_featured'] ?? false),
|
||||
'metrics' => [
|
||||
'views' => (int) ($item['views_count'] ?? 0),
|
||||
'appreciation' => (int) (($item['likes_count'] ?? 0) + ($item['followers_count'] ?? 0)),
|
||||
'shares' => (int) ($item['shares_count'] ?? 0),
|
||||
'comments' => (int) ($item['comments_count'] ?? 0),
|
||||
'saves' => (int) ($item['saves_count'] ?? 0),
|
||||
],
|
||||
'engagement_score' => (int) ($item['views_count'] ?? 0)
|
||||
+ ((int) ($item['likes_count'] ?? 0) * 2)
|
||||
+ ((int) ($item['followers_count'] ?? 0) * 2)
|
||||
+ ((int) ($item['comments_count'] ?? 0) * 3)
|
||||
+ ((int) ($item['shares_count'] ?? 0) * 2)
|
||||
+ ((int) ($item['saves_count'] ?? 0) * 2),
|
||||
'taxonomies' => [
|
||||
'categories' => [],
|
||||
'tags' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function actionsFor(array $item, string $status): array
|
||||
{
|
||||
$collectionId = (int) $item['id'];
|
||||
$actions = [];
|
||||
$featured = (bool) ($item['is_featured'] ?? false);
|
||||
|
||||
if ($status === 'draft') {
|
||||
$actions[] = $this->requestAction(
|
||||
'publish',
|
||||
'Publish',
|
||||
'fa-solid fa-rocket',
|
||||
route('settings.collections.lifecycle', ['collection' => $collectionId]),
|
||||
[
|
||||
'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
|
||||
'visibility' => Collection::VISIBILITY_PUBLIC,
|
||||
'published_at' => now()->toIso8601String(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (in_array($status, ['published', 'scheduled'], true)) {
|
||||
$actions[] = $this->requestAction(
|
||||
'archive',
|
||||
'Archive',
|
||||
'fa-solid fa-box-archive',
|
||||
route('settings.collections.lifecycle', ['collection' => $collectionId]),
|
||||
[
|
||||
'lifecycle_state' => Collection::LIFECYCLE_ARCHIVED,
|
||||
'archived_at' => now()->toIso8601String(),
|
||||
]
|
||||
);
|
||||
|
||||
$actions[] = $featured
|
||||
? $this->requestAction('unfeature', 'Remove feature', 'fa-solid fa-star-half-stroke', route('settings.collections.unfeature', ['collection' => $collectionId]), [], null, 'delete')
|
||||
: $this->requestAction('feature', 'Feature', 'fa-solid fa-star', route('settings.collections.feature', ['collection' => $collectionId]), []);
|
||||
|
||||
if ($status === 'scheduled') {
|
||||
$actions[] = $this->requestAction('publish_now', 'Publish now', 'fa-solid fa-bolt', route('api.studio.schedule.publishNow', ['module' => 'collections', 'id' => $collectionId]), []);
|
||||
$actions[] = $this->requestAction('unschedule', 'Unschedule', 'fa-solid fa-calendar-xmark', route('api.studio.schedule.unschedule', ['module' => 'collections', 'id' => $collectionId]), []);
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === 'archived') {
|
||||
$actions[] = $this->requestAction(
|
||||
'restore',
|
||||
'Restore',
|
||||
'fa-solid fa-rotate-left',
|
||||
route('settings.collections.lifecycle', ['collection' => $collectionId]),
|
||||
[
|
||||
'lifecycle_state' => Collection::LIFECYCLE_DRAFT,
|
||||
'visibility' => Collection::VISIBILITY_PRIVATE,
|
||||
'archived_at' => null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$actions[] = $this->requestAction(
|
||||
'delete',
|
||||
'Delete',
|
||||
'fa-solid fa-trash',
|
||||
route('settings.collections.destroy', ['collection' => $collectionId]),
|
||||
[],
|
||||
'Delete this collection permanently?',
|
||||
'delete'
|
||||
);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
private function requestAction(string $key, string $label, string $icon, string $url, array $payload = [], ?string $confirm = null, string $method = 'post'): array
|
||||
{
|
||||
return [
|
||||
'key' => $key,
|
||||
'label' => $label,
|
||||
'icon' => $icon,
|
||||
'type' => 'request',
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'payload' => $payload,
|
||||
'confirm' => $confirm,
|
||||
];
|
||||
}
|
||||
}
|
||||
277
app/Services/Studio/Providers/StoryStudioProvider.php
Normal file
277
app/Services/Studio/Providers/StoryStudioProvider.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Studio\Providers;
|
||||
|
||||
use App\Models\Story;
|
||||
use App\Models\User;
|
||||
use App\Services\Studio\Contracts\CreatorStudioProvider;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class StoryStudioProvider implements CreatorStudioProvider
|
||||
{
|
||||
public function key(): string
|
||||
{
|
||||
return 'stories';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Stories';
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return 'fa-solid fa-feather-pointed';
|
||||
}
|
||||
|
||||
public function createUrl(): string
|
||||
{
|
||||
return route('creator.stories.create');
|
||||
}
|
||||
|
||||
public function indexUrl(): string
|
||||
{
|
||||
return route('studio.stories');
|
||||
}
|
||||
|
||||
public function summary(User $user): array
|
||||
{
|
||||
$baseQuery = Story::query()->where('creator_id', $user->id);
|
||||
|
||||
$count = (clone $baseQuery)
|
||||
->whereNotIn('status', ['archived'])
|
||||
->count();
|
||||
|
||||
$draftCount = (clone $baseQuery)
|
||||
->whereIn('status', ['draft', 'pending_review', 'rejected'])
|
||||
->count();
|
||||
|
||||
$publishedCount = (clone $baseQuery)
|
||||
->whereIn('status', ['published', 'scheduled'])
|
||||
->count();
|
||||
|
||||
$recentPublishedCount = (clone $baseQuery)
|
||||
->whereIn('status', ['published', 'scheduled'])
|
||||
->where('published_at', '>=', now()->subDays(30))
|
||||
->count();
|
||||
|
||||
$archivedCount = (clone $baseQuery)
|
||||
->where('status', 'archived')
|
||||
->count();
|
||||
|
||||
return [
|
||||
'key' => $this->key(),
|
||||
'label' => $this->label(),
|
||||
'icon' => $this->icon(),
|
||||
'count' => $count,
|
||||
'draft_count' => $draftCount,
|
||||
'published_count' => $publishedCount,
|
||||
'archived_count' => $archivedCount,
|
||||
'trend_value' => $recentPublishedCount,
|
||||
'trend_label' => 'published in 30d',
|
||||
'quick_action_label' => 'Create story',
|
||||
'index_url' => $this->indexUrl(),
|
||||
'create_url' => $this->createUrl(),
|
||||
];
|
||||
}
|
||||
|
||||
public function items(User $user, string $bucket = 'all', int $limit = 200): Collection
|
||||
{
|
||||
$query = Story::query()
|
||||
->where('creator_id', $user->id)
|
||||
->with(['tags'])
|
||||
->orderByDesc('updated_at')
|
||||
->limit($limit);
|
||||
|
||||
if ($bucket === 'drafts') {
|
||||
$query->whereIn('status', ['draft', 'pending_review', 'rejected']);
|
||||
} elseif ($bucket === 'scheduled') {
|
||||
$query->where('status', 'scheduled');
|
||||
} elseif ($bucket === 'archived') {
|
||||
$query->where('status', 'archived');
|
||||
} elseif ($bucket === 'published') {
|
||||
$query->whereIn('status', ['published', 'scheduled']);
|
||||
} else {
|
||||
$query->where('status', '!=', 'archived');
|
||||
}
|
||||
|
||||
return $query->get()->map(fn (Story $story): array => $this->mapItem($story));
|
||||
}
|
||||
|
||||
public function topItems(User $user, int $limit = 5): Collection
|
||||
{
|
||||
return Story::query()
|
||||
->where('creator_id', $user->id)
|
||||
->whereIn('status', ['published', 'scheduled'])
|
||||
->orderByDesc('views')
|
||||
->orderByDesc('likes_count')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn (Story $story): array => $this->mapItem($story));
|
||||
}
|
||||
|
||||
public function analytics(User $user): array
|
||||
{
|
||||
$totals = Story::query()
|
||||
->where('creator_id', $user->id)
|
||||
->where('status', '!=', 'archived')
|
||||
->selectRaw('COALESCE(SUM(views), 0) as views')
|
||||
->selectRaw('COALESCE(SUM(likes_count), 0) as appreciation')
|
||||
->selectRaw('0 as shares')
|
||||
->selectRaw('COALESCE(SUM(comments_count), 0) as comments')
|
||||
->selectRaw('0 as saves')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'views' => (int) ($totals->views ?? 0),
|
||||
'appreciation' => (int) ($totals->appreciation ?? 0),
|
||||
'shares' => 0,
|
||||
'comments' => (int) ($totals->comments ?? 0),
|
||||
'saves' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function scheduledItems(User $user, int $limit = 50): Collection
|
||||
{
|
||||
return $this->items($user, 'scheduled', $limit);
|
||||
}
|
||||
|
||||
private function mapItem(Story $story): array
|
||||
{
|
||||
$subtitle = $story->story_type ? ucfirst(str_replace('_', ' ', (string) $story->story_type)) : null;
|
||||
$viewUrl = in_array($story->status, ['published', 'scheduled'], true)
|
||||
? route('stories.show', ['slug' => $story->slug])
|
||||
: route('creator.stories.preview', ['story' => $story->id]);
|
||||
|
||||
return [
|
||||
'id' => sprintf('%s:%d', $this->key(), (int) $story->id),
|
||||
'numeric_id' => (int) $story->id,
|
||||
'module' => $this->key(),
|
||||
'module_label' => $this->label(),
|
||||
'module_icon' => $this->icon(),
|
||||
'title' => $story->title,
|
||||
'subtitle' => $subtitle,
|
||||
'description' => $story->excerpt,
|
||||
'status' => $story->status,
|
||||
'visibility' => $story->status === 'published' ? 'public' : 'private',
|
||||
'image_url' => $story->cover_url,
|
||||
'preview_url' => route('creator.stories.preview', ['story' => $story->id]),
|
||||
'view_url' => $viewUrl,
|
||||
'edit_url' => route('creator.stories.edit', ['story' => $story->id]),
|
||||
'manage_url' => route('creator.stories.edit', ['story' => $story->id]),
|
||||
'analytics_url' => route('creator.stories.analytics', ['story' => $story->id]),
|
||||
'create_url' => $this->createUrl(),
|
||||
'actions' => $this->actionsFor($story, $story->status),
|
||||
'created_at' => $story->created_at?->toIso8601String(),
|
||||
'updated_at' => $story->updated_at?->toIso8601String(),
|
||||
'published_at' => $story->published_at?->toIso8601String(),
|
||||
'scheduled_at' => $story->scheduled_for?->toIso8601String(),
|
||||
'featured' => (bool) $story->featured,
|
||||
'activity_state' => in_array($story->status, ['published', 'scheduled'], true)
|
||||
? 'active'
|
||||
: ($story->status === 'archived' ? 'archived' : 'inactive'),
|
||||
'metrics' => [
|
||||
'views' => (int) $story->views,
|
||||
'appreciation' => (int) $story->likes_count,
|
||||
'shares' => 0,
|
||||
'comments' => (int) $story->comments_count,
|
||||
'saves' => 0,
|
||||
],
|
||||
'engagement_score' => (int) $story->views
|
||||
+ ((int) $story->likes_count * 2)
|
||||
+ ((int) $story->comments_count * 3),
|
||||
'taxonomies' => [
|
||||
'categories' => [],
|
||||
'tags' => $story->tags->map(fn ($entry): array => [
|
||||
'id' => (int) $entry->id,
|
||||
'name' => (string) $entry->name,
|
||||
'slug' => (string) $entry->slug,
|
||||
])->values()->all(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function actionsFor(Story $story, string $status): array
|
||||
{
|
||||
$actions = [];
|
||||
|
||||
if (in_array($status, ['draft', 'pending_review', 'rejected'], true)) {
|
||||
$actions[] = $this->requestAction(
|
||||
'publish',
|
||||
'Publish',
|
||||
'fa-solid fa-rocket',
|
||||
route('api.stories.update'),
|
||||
[
|
||||
'story_id' => (int) $story->id,
|
||||
'status' => 'published',
|
||||
],
|
||||
null,
|
||||
'put'
|
||||
);
|
||||
}
|
||||
|
||||
if (in_array($status, ['draft', 'pending_review', 'rejected', 'published', 'scheduled'], true)) {
|
||||
$actions[] = $this->requestAction(
|
||||
'archive',
|
||||
'Archive',
|
||||
'fa-solid fa-box-archive',
|
||||
route('api.stories.update'),
|
||||
[
|
||||
'story_id' => (int) $story->id,
|
||||
'status' => 'archived',
|
||||
],
|
||||
null,
|
||||
'put'
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 'scheduled') {
|
||||
$actions[] = $this->requestAction('publish_now', 'Publish now', 'fa-solid fa-bolt', route('api.studio.schedule.publishNow', ['module' => 'stories', 'id' => $story->id]), []);
|
||||
$actions[] = $this->requestAction('unschedule', 'Unschedule', 'fa-solid fa-calendar-xmark', route('api.studio.schedule.unschedule', ['module' => 'stories', 'id' => $story->id]), []);
|
||||
}
|
||||
|
||||
if ($status === 'archived') {
|
||||
$actions[] = $this->requestAction(
|
||||
'restore',
|
||||
'Restore',
|
||||
'fa-solid fa-rotate-left',
|
||||
route('api.stories.update'),
|
||||
[
|
||||
'story_id' => (int) $story->id,
|
||||
'status' => 'draft',
|
||||
],
|
||||
null,
|
||||
'put'
|
||||
);
|
||||
}
|
||||
|
||||
$actions[] = $this->requestAction(
|
||||
'delete',
|
||||
'Delete',
|
||||
'fa-solid fa-trash',
|
||||
route('creator.stories.destroy', ['story' => $story->id]),
|
||||
[],
|
||||
'Delete this story permanently?',
|
||||
'delete'
|
||||
);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
private function requestAction(string $key, string $label, string $icon, string $url, array $payload = [], ?string $confirm = null, string $method = 'post'): array
|
||||
{
|
||||
return [
|
||||
'key' => $key,
|
||||
'label' => $label,
|
||||
'icon' => $icon,
|
||||
'type' => 'request',
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'payload' => $payload,
|
||||
'confirm' => $confirm,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user