Upload wizard:
- Refactored UploadWizard into modular steps (Step1FileUpload, Step2Details, Step3Publish)
- Extracted reusable hooks: useUploadMachine, useFileValidation, useVisionTags
- Extracted reusable components: CategorySelector, ContentTypeSelector
- Added TagPicker component (studio-style list picker with AI badge + new-tag insertion)
- Fixed TagInput auto-open bug (hasFocusedRef guard)
- Replaced TagInput with TagPicker in UploadSidebar
Vision AI tag suggestions:
- Add UploadVisionSuggestController: sync POST /api/uploads/{id}/vision-suggest
- Calls vision.klevze.net/analyze/all on upload completion (before step 2 opens)
- Two-phase useVisionTags: immediate gateway call + background DB polling
- Trigger fires on uploadReady (not step change) so tags arrive before user sees step 2
- Added vision.gateway config block with VISION_GATEWAY_URL env
Artwork versioning system:
- ArtworkVersion / ArtworkVersionEvent models
- ArtworkVersioningService: createNewVersion, restoreVersion, rate limiting, ranking decay
- Migrations: artwork_versions, artwork_version_events, versioning columns on artworks
- Studio API routes: GET versions, POST restore/{version_id}
- Feature tests: ArtworkVersioningTest (13 cases)
178 lines
6.4 KiB
PHP
178 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Studio;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Category;
|
|
use App\Models\ContentType;
|
|
use App\Services\Studio\StudioMetricsService;
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
/**
|
|
* Serves Studio Inertia pages for authenticated creators.
|
|
*/
|
|
final class StudioController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly StudioMetricsService $metrics,
|
|
) {}
|
|
|
|
/**
|
|
* Studio Overview Dashboard (/studio)
|
|
*/
|
|
public function index(Request $request): Response
|
|
{
|
|
$userId = $request->user()->id;
|
|
|
|
return Inertia::render('Studio/StudioDashboard', [
|
|
'kpis' => $this->metrics->getDashboardKpis($userId),
|
|
'topPerformers' => $this->metrics->getTopPerformers($userId, 6),
|
|
'recentComments' => $this->metrics->getRecentComments($userId, 5),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Artwork Manager (/studio/artworks)
|
|
*/
|
|
public function artworks(Request $request): Response
|
|
{
|
|
return Inertia::render('Studio/StudioArtworks', [
|
|
'categories' => $this->getCategories(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Drafts (/studio/artworks/drafts)
|
|
*/
|
|
public function drafts(Request $request): Response
|
|
{
|
|
return Inertia::render('Studio/StudioDrafts', [
|
|
'categories' => $this->getCategories(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Archived (/studio/artworks/archived)
|
|
*/
|
|
public function archived(Request $request): Response
|
|
{
|
|
return Inertia::render('Studio/StudioArchived', [
|
|
'categories' => $this->getCategories(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Edit artwork (/studio/artworks/:id/edit)
|
|
*/
|
|
public function edit(Request $request, int $id): Response
|
|
{
|
|
$artwork = $request->user()->artworks()
|
|
->with(['stats', 'categories.contentType', 'tags'])
|
|
->findOrFail($id);
|
|
|
|
$primaryCategory = $artwork->categories->first();
|
|
|
|
return Inertia::render('Studio/StudioArtworkEdit', [
|
|
'artwork' => [
|
|
'id' => $artwork->id,
|
|
'title' => $artwork->title,
|
|
'slug' => $artwork->slug,
|
|
'description' => $artwork->description,
|
|
'is_public' => (bool) $artwork->is_public,
|
|
'is_approved' => (bool) $artwork->is_approved,
|
|
'thumb_url' => $artwork->thumbUrl('md'),
|
|
'thumb_url_lg' => $artwork->thumbUrl('lg'),
|
|
'file_name' => $artwork->file_name,
|
|
'file_size' => $artwork->file_size,
|
|
'width' => $artwork->width,
|
|
'height' => $artwork->height,
|
|
'mime_type' => $artwork->mime_type,
|
|
'content_type_id' => $primaryCategory?->contentType?->id,
|
|
'category_id' => $primaryCategory?->id,
|
|
'parent_category_id' => $primaryCategory?->parent_id ? $primaryCategory->parent_id : $primaryCategory?->id,
|
|
'sub_category_id' => $primaryCategory?->parent_id ? $primaryCategory->id : null,
|
|
'categories' => $artwork->categories->map(fn ($c) => ['id' => $c->id, 'name' => $c->name, 'slug' => $c->slug])->values()->all(),
|
|
'tags' => $artwork->tags->map(fn ($t) => ['id' => $t->id, 'name' => $t->name, 'slug' => $t->slug])->values()->all(),
|
|
// Versioning
|
|
'version_count' => (int) ($artwork->version_count ?? 1),
|
|
'requires_reapproval' => (bool) $artwork->requires_reapproval,
|
|
],
|
|
'contentTypes' => $this->getCategories(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Analytics v1 (/studio/artworks/:id/analytics)
|
|
*/
|
|
public function analytics(Request $request, int $id): Response
|
|
{
|
|
$artwork = $request->user()->artworks()
|
|
->with(['stats', 'awardStat'])
|
|
->findOrFail($id);
|
|
|
|
$stats = $artwork->stats;
|
|
|
|
return Inertia::render('Studio/StudioArtworkAnalytics', [
|
|
'artwork' => [
|
|
'id' => $artwork->id,
|
|
'title' => $artwork->title,
|
|
'slug' => $artwork->slug,
|
|
'thumb_url' => $artwork->thumbUrl('md'),
|
|
],
|
|
'analytics' => [
|
|
'views' => (int) ($stats?->views ?? 0),
|
|
'favourites' => (int) ($stats?->favorites ?? 0),
|
|
'shares' => (int) ($stats?->shares_count ?? 0),
|
|
'comments' => (int) ($stats?->comments_count ?? 0),
|
|
'downloads' => (int) ($stats?->downloads ?? 0),
|
|
'ranking_score' => (float) ($stats?->ranking_score ?? 0),
|
|
'heat_score' => (float) ($stats?->heat_score ?? 0),
|
|
'engagement_velocity' => (float) ($stats?->engagement_velocity ?? 0),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Studio-wide Analytics (/studio/analytics)
|
|
*/
|
|
public function analyticsOverview(Request $request): Response
|
|
{
|
|
$userId = $request->user()->id;
|
|
$data = $this->metrics->getAnalyticsOverview($userId);
|
|
|
|
return Inertia::render('Studio/StudioAnalytics', [
|
|
'totals' => $data['totals'],
|
|
'topArtworks' => $data['top_artworks'],
|
|
'contentBreakdown' => $data['content_breakdown'],
|
|
'recentComments' => $this->metrics->getRecentComments($userId, 8),
|
|
]);
|
|
}
|
|
|
|
private function getCategories(): array
|
|
{
|
|
return ContentType::with(['rootCategories.children'])->get()->map(function ($ct) {
|
|
return [
|
|
'id' => $ct->id,
|
|
'name' => $ct->name,
|
|
'slug' => $ct->slug,
|
|
'categories' => $ct->rootCategories->map(function ($c) {
|
|
return [
|
|
'id' => $c->id,
|
|
'name' => $c->name,
|
|
'slug' => $c->slug,
|
|
'children' => $c->children->map(fn ($ch) => [
|
|
'id' => $ch->id,
|
|
'name' => $ch->name,
|
|
'slug' => $ch->slug,
|
|
])->values()->all(),
|
|
];
|
|
})->values()->all(),
|
|
];
|
|
})->values()->all();
|
|
}
|
|
}
|