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

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Artwork;
use App\Models\ContentType;
use Illuminate\Http\Response;
use Illuminate\View\View;
/**
* RssFeedController
*
* GET /rss-feeds info page listing available feeds
* GET /rss/latest-uploads.xml all published artworks
* GET /rss/latest-skins.xml skins only
* GET /rss/latest-wallpapers.xml wallpapers only
* GET /rss/latest-photos.xml photography only
*/
final class RssFeedController extends Controller
{
/** Number of items per feed. */
private const FEED_LIMIT = 25;
/** Feed definitions shown on the info page. */
public const FEEDS = [
'uploads' => ['title' => 'Latest Uploads', 'url' => '/rss/latest-uploads.xml'],
'skins' => ['title' => 'Latest Skins', 'url' => '/rss/latest-skins.xml'],
'wallpapers' => ['title' => 'Latest Wallpapers', 'url' => '/rss/latest-wallpapers.xml'],
'photos' => ['title' => 'Latest Photos', 'url' => '/rss/latest-photos.xml'],
];
/** Info page at /rss-feeds */
public function index(): View
{
return view('web.rss-feeds', [
'page_title' => 'RSS Feeds — Skinbase',
'page_meta_description' => 'Subscribe to Skinbase RSS feeds to stay up to date with the latest uploads, skins, wallpapers, and photos.',
'page_canonical' => url('/rss-feeds'),
'hero_title' => 'RSS Feeds',
'hero_description' => 'Subscribe to stay up to date with the latest content on Skinbase.',
'breadcrumbs' => collect([
(object) ['name' => 'Home', 'url' => '/'],
(object) ['name' => 'RSS Feeds', 'url' => '/rss-feeds'],
]),
'feeds' => self::FEEDS,
'center_content' => true,
'center_max' => '3xl',
]);
}
/** /rss/latest-uploads.xml — all content types */
public function latestUploads(): Response
{
$artworks = Artwork::published()
->with(['user'])
->latest('published_at')
->limit(self::FEED_LIMIT)
->get();
return $this->buildFeed('Latest Uploads', url('/rss/latest-uploads.xml'), $artworks);
}
/** /rss/latest-skins.xml */
public function latestSkins(): Response
{
return $this->feedByContentType('skins', 'Latest Skins', '/rss/latest-skins.xml');
}
/** /rss/latest-wallpapers.xml */
public function latestWallpapers(): Response
{
return $this->feedByContentType('wallpapers', 'Latest Wallpapers', '/rss/latest-wallpapers.xml');
}
/** /rss/latest-photos.xml */
public function latestPhotos(): Response
{
return $this->feedByContentType('photography', 'Latest Photos', '/rss/latest-photos.xml');
}
// -------------------------------------------------------------------------
private function feedByContentType(string $slug, string $title, string $feedPath): Response
{
$contentType = ContentType::where('slug', $slug)->first();
$query = Artwork::published()->with(['user'])->latest('published_at')->limit(self::FEED_LIMIT);
if ($contentType) {
$query->whereHas('categories', fn ($q) => $q->where('content_type_id', $contentType->id));
}
return $this->buildFeed($title, url($feedPath), $query->get());
}
private function buildFeed(string $channelTitle, string $feedUrl, $artworks): Response
{
$content = view('rss.feed', [
'channelTitle' => $channelTitle . ' — Skinbase',
'channelDescription' => 'The latest ' . strtolower($channelTitle) . ' from Skinbase.org',
'channelLink' => url('/'),
'feedUrl' => $feedUrl,
'artworks' => $artworks,
'buildDate' => now()->toRfc2822String(),
])->render();
return response($content, 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8',
]);
}
}