feat: Inertia profile settings page, Studio edit redesign, EGS, Nova UI components\n\n- Redesign /dashboard/profile as Inertia React page (Settings/ProfileEdit)\n with SettingsLayout sidebar, Nova UI components (TextInput, Textarea,\n Toggle, Select, RadioGroup, Modal, Button), avatar drag-and-drop,\n password change, and account deletion sections\n- Redesign Studio artwork edit page with two-column layout, Nova components,\n integrated TagPicker, and version history modal\n- Add shared MarkdownEditor component\n- Add Early-Stage Growth System (EGS): SpotlightEngine, FeedBlender,\n GridFiller, AdaptiveTimeWindow, ActivityLayer, admin panel\n- Fix upload category/tag persistence (V1+V2 paths)\n- Fix tag source enum, category tree display, binding resolution\n- Add settings.jsx Vite entry, settings.blade.php wrapper\n- Update ProfileController with JSON response support for API calls\n- Various route fixes (profile.edit, toolbar settings link)"

This commit is contained in:
2026-03-03 20:57:43 +01:00
parent dc51d65440
commit b9c2d8597d
114 changed files with 8760 additions and 693 deletions

View File

@@ -9,15 +9,77 @@ use App\Http\Resources\ArtworkResource;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use App\Services\ThumbnailPresenter;
use App\Services\ErrorSuggestionService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class ArtworkPageController extends Controller
{
public function show(Request $request, int $id, ?string $slug = null): View|RedirectResponse
public function show(Request $request, int $id, ?string $slug = null): View|RedirectResponse|Response
{
// ── Step 1: check existence including soft-deleted ─────────────────
$raw = Artwork::withTrashed()->where('id', $id)->first();
if (! $raw) {
// Artwork never existed → contextual 404
$suggestions = app(ErrorSuggestionService::class);
return response(view('errors.contextual.artwork-not-found', [
'trendingArtworks' => $this->safeSuggestions(fn () => $suggestions->trendingArtworks()),
]), 404);
}
if ($raw->trashed()) {
// Artwork permanently deleted → 410 Gone
return response(view('errors.410'), 410);
}
if (! $raw->is_public || ! $raw->is_approved) {
// Artwork exists but is private/unapproved → 403 Forbidden.
// Show other public artworks by the same creator as recovery suggestions.
$suggestions = app(ErrorSuggestionService::class);
$creatorArtworks = collect();
$creatorUsername = null;
if ($raw->user_id) {
$raw->loadMissing('user');
$creatorUsername = $raw->user?->username;
$creatorArtworks = $this->safeSuggestions(function () use ($raw) {
return Artwork::query()
->with('user')
->where('user_id', $raw->user_id)
->where('id', '!=', $raw->id)
->public()
->published()
->limit(6)
->get()
->map(function (Artwork $a) {
$slug = \Illuminate\Support\Str::slug((string) ($a->slug ?: $a->title)) ?: (string) $a->id;
$md = \App\Services\ThumbnailPresenter::present($a, 'md');
return [
'id' => $a->id,
'title' => html_entity_decode((string) $a->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'author' => html_entity_decode((string) ($a->user?->name ?: $a->user?->username ?: 'Artist'), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'url' => route('art.show', ['id' => $a->id, 'slug' => $slug]),
'thumb' => $md['url'] ?? null,
];
});
});
}
return response(view('errors.contextual.artwork-not-found', [
'message' => 'This artwork is not publicly available.',
'isForbidden' => true,
'creatorArtworks' => $creatorArtworks,
'creatorUsername' => $creatorUsername,
'trendingArtworks' => $this->safeSuggestions(fn () => $suggestions->trendingArtworks()),
]), 403);
}
// ── Step 2: full load with all relations ───────────────────────────
$artwork = Artwork::with(['user.profile', 'categories.contentType', 'categories.parent.contentType', 'tags', 'stats'])
->where('id', $id)
->public()
@@ -150,4 +212,14 @@ final class ArtworkPageController extends Controller
'comments' => $comments,
]);
}
/** Silently catch suggestion query failures so error page never crashes. */
private function safeSuggestions(callable $fn): mixed
{
try {
return $fn();
} catch (\Throwable) {
return collect();
}
}
}