login update
This commit is contained in:
@@ -13,18 +13,66 @@ 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
|
||||
* GET /rss-feeds → info page listing all available feeds
|
||||
* GET /rss/latest-uploads.xml → all published artworks (legacy)
|
||||
* GET /rss/latest-skins.xml → skins only (legacy)
|
||||
* GET /rss/latest-wallpapers.xml → wallpapers only (legacy)
|
||||
* GET /rss/latest-photos.xml → photography only (legacy)
|
||||
*
|
||||
* Nova feeds live in App\Http\Controllers\RSS\*.
|
||||
*/
|
||||
final class RssFeedController extends Controller
|
||||
{
|
||||
/** Number of items per feed. */
|
||||
/** Number of items per legacy feed. */
|
||||
private const FEED_LIMIT = 25;
|
||||
|
||||
/** Feed definitions shown on the info page. */
|
||||
/**
|
||||
* Grouped feed definitions shown on the /rss-feeds info page.
|
||||
* Each group has a 'label' and an array of 'feeds' with title + url.
|
||||
*/
|
||||
public const FEED_GROUPS = [
|
||||
'global' => [
|
||||
'label' => 'Global',
|
||||
'feeds' => [
|
||||
['title' => 'Latest Artworks', 'url' => '/rss', 'description' => 'All new artworks across the platform.'],
|
||||
],
|
||||
],
|
||||
'discover' => [
|
||||
'label' => 'Discover',
|
||||
'feeds' => [
|
||||
['title' => 'Fresh Uploads', 'url' => '/rss/discover/fresh', 'description' => 'The newest artworks just published.'],
|
||||
['title' => 'Trending', 'url' => '/rss/discover/trending', 'description' => 'Most-viewed artworks over the past 7 days.'],
|
||||
['title' => 'Rising', 'url' => '/rss/discover/rising', 'description' => 'Artworks gaining momentum right now.'],
|
||||
],
|
||||
],
|
||||
'explore' => [
|
||||
'label' => 'Explore',
|
||||
'feeds' => [
|
||||
['title' => 'All Artworks', 'url' => '/rss/explore/artworks', 'description' => 'Latest artworks of all types.'],
|
||||
['title' => 'Wallpapers', 'url' => '/rss/explore/wallpapers', 'description' => 'Latest wallpapers.'],
|
||||
['title' => 'Skins', 'url' => '/rss/explore/skins', 'description' => 'Latest skins.'],
|
||||
['title' => 'Photography', 'url' => '/rss/explore/photography', 'description' => 'Latest photography.'],
|
||||
['title' => 'Trending Wallpapers', 'url' => '/rss/explore/wallpapers/trending', 'description' => 'Trending wallpapers this week.'],
|
||||
],
|
||||
],
|
||||
'blog' => [
|
||||
'label' => 'Blog',
|
||||
'feeds' => [
|
||||
['title' => 'Blog Posts', 'url' => '/rss/blog', 'description' => 'Latest posts from the Skinbase blog.'],
|
||||
],
|
||||
],
|
||||
'legacy' => [
|
||||
'label' => 'Legacy Feeds',
|
||||
'feeds' => [
|
||||
['title' => 'Latest Uploads (XML)', 'url' => '/rss/latest-uploads.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Skins (XML)', 'url' => '/rss/latest-skins.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Wallpapers (XML)', 'url' => '/rss/latest-wallpapers.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Photos (XML)', 'url' => '/rss/latest-photos.xml', 'description' => 'Legacy XML feed.'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/** Flat feed list kept for backward-compatibility (old view logic). */
|
||||
public const FEEDS = [
|
||||
'uploads' => ['title' => 'Latest Uploads', 'url' => '/rss/latest-uploads.xml'],
|
||||
'skins' => ['title' => 'Latest Skins', 'url' => '/rss/latest-skins.xml'],
|
||||
@@ -45,7 +93,8 @@ final class RssFeedController extends Controller
|
||||
(object) ['name' => 'Home', 'url' => '/'],
|
||||
(object) ['name' => 'RSS Feeds', 'url' => '/rss-feeds'],
|
||||
]),
|
||||
'feeds' => self::FEEDS,
|
||||
'feeds' => self::FEEDS,
|
||||
'feed_groups' => self::FEED_GROUPS,
|
||||
'center_content' => true,
|
||||
'center_max' => '3xl',
|
||||
]);
|
||||
|
||||
59
app/Http/Controllers/Web/StoriesAuthorController.php
Normal file
59
app/Http/Controllers/Web/StoriesAuthorController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Story;
|
||||
use App\Models\StoryAuthor;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Stories filtered by author — /stories/author/{username}
|
||||
*/
|
||||
final class StoriesAuthorController extends Controller
|
||||
{
|
||||
public function show(Request $request, string $username): View
|
||||
{
|
||||
// Resolve by linked user username first, then by author name slug
|
||||
$author = StoryAuthor::whereHas('user', fn ($q) => $q->where('username', $username))
|
||||
->with('user')
|
||||
->first();
|
||||
|
||||
if (! $author) {
|
||||
// Fallback: author name matches slug-style
|
||||
$author = StoryAuthor::where('name', $username)->first();
|
||||
}
|
||||
|
||||
if (! $author) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$stories = Cache::remember('stories:author:' . $author->id . ':page:' . ($request->get('page', 1)), 300, fn () =>
|
||||
Story::published()
|
||||
->with('author', 'tags')
|
||||
->where('author_id', $author->id)
|
||||
->orderByDesc('published_at')
|
||||
->paginate(12)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
$authorName = $author->user?->username ?? $author->name;
|
||||
|
||||
return view('web.stories.author', [
|
||||
'author' => $author,
|
||||
'stories' => $stories,
|
||||
'page_title' => 'Stories by ' . $authorName . ' — Skinbase',
|
||||
'page_meta_description' => 'All stories and interviews by ' . $authorName . ' on Skinbase.',
|
||||
'page_canonical' => url('/stories/author/' . $username),
|
||||
'page_robots' => 'index,follow',
|
||||
'breadcrumbs' => collect([
|
||||
(object) ['name' => 'Stories', 'url' => '/stories'],
|
||||
(object) ['name' => $authorName, 'url' => '/stories/author/' . $username],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/Web/StoriesController.php
Normal file
47
app/Http/Controllers/Web/StoriesController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Story;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Stories listing page — /stories
|
||||
*/
|
||||
final class StoriesController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$featured = Cache::remember('stories:featured', 300, fn () =>
|
||||
Story::published()->featured()
|
||||
->with('author', 'tags')
|
||||
->orderByDesc('published_at')
|
||||
->first()
|
||||
);
|
||||
|
||||
$stories = Cache::remember('stories:list:page:' . ($request->get('page', 1)), 300, fn () =>
|
||||
Story::published()
|
||||
->with('author', 'tags')
|
||||
->orderByDesc('published_at')
|
||||
->paginate(12)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
return view('web.stories.index', [
|
||||
'featured' => $featured,
|
||||
'stories' => $stories,
|
||||
'page_title' => 'Stories — Skinbase',
|
||||
'page_meta_description' => 'Artist interviews, community spotlights, tutorials and announcements from Skinbase.',
|
||||
'page_canonical' => url('/stories'),
|
||||
'page_robots' => 'index,follow',
|
||||
'breadcrumbs' => collect([
|
||||
(object) ['name' => 'Stories', 'url' => '/stories'],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
app/Http/Controllers/Web/StoriesTagController.php
Normal file
45
app/Http/Controllers/Web/StoriesTagController.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Story;
|
||||
use App\Models\StoryTag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Stories filtered by tag — /stories/tag/{tag}
|
||||
*/
|
||||
final class StoriesTagController extends Controller
|
||||
{
|
||||
public function show(Request $request, string $tag): View
|
||||
{
|
||||
$storyTag = StoryTag::where('slug', $tag)->firstOrFail();
|
||||
|
||||
$stories = Cache::remember('stories:tag:' . $tag . ':page:' . ($request->get('page', 1)), 300, fn () =>
|
||||
Story::published()
|
||||
->with('author', 'tags')
|
||||
->whereHas('tags', fn ($q) => $q->where('stories_tags.id', $storyTag->id))
|
||||
->orderByDesc('published_at')
|
||||
->paginate(12)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
return view('web.stories.tag', [
|
||||
'storyTag' => $storyTag,
|
||||
'stories' => $stories,
|
||||
'page_title' => '#' . $storyTag->name . ' Stories — Skinbase',
|
||||
'page_meta_description' => 'Stories tagged with "' . $storyTag->name . '" on Skinbase.',
|
||||
'page_canonical' => url('/stories/tag/' . $storyTag->slug),
|
||||
'page_robots' => 'index,follow',
|
||||
'breadcrumbs' => collect([
|
||||
(object) ['name' => 'Stories', 'url' => '/stories'],
|
||||
(object) ['name' => '#' . $storyTag->name, 'url' => '/stories/tag/' . $storyTag->slug],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
86
app/Http/Controllers/Web/StoryController.php
Normal file
86
app/Http/Controllers/Web/StoryController.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Story;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Single story page — /stories/{slug}
|
||||
*/
|
||||
final class StoryController extends Controller
|
||||
{
|
||||
public function show(string $slug): View
|
||||
{
|
||||
$story = Cache::remember('stories:' . $slug, 600, fn () =>
|
||||
Story::published()
|
||||
->with('author', 'tags')
|
||||
->where('slug', $slug)
|
||||
->firstOrFail()
|
||||
);
|
||||
|
||||
// Increment view counter (fire-and-forget, no cache invalidation needed)
|
||||
Story::where('id', $story->id)->increment('views');
|
||||
|
||||
// Related stories: shared tags → same author → newest
|
||||
$related = Cache::remember('stories:related:' . $story->id, 600, function () use ($story) {
|
||||
$tagIds = $story->tags->pluck('id');
|
||||
|
||||
$related = collect();
|
||||
|
||||
if ($tagIds->isNotEmpty()) {
|
||||
$related = Story::published()
|
||||
->with('author', 'tags')
|
||||
->whereHas('tags', fn ($q) => $q->whereIn('stories_tags.id', $tagIds))
|
||||
->where('id', '!=', $story->id)
|
||||
->orderByDesc('published_at')
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
if ($related->count() < 3 && $story->author_id) {
|
||||
$byAuthor = Story::published()
|
||||
->with('author', 'tags')
|
||||
->where('author_id', $story->author_id)
|
||||
->where('id', '!=', $story->id)
|
||||
->whereNotIn('id', $related->pluck('id'))
|
||||
->orderByDesc('published_at')
|
||||
->limit(6 - $related->count())
|
||||
->get();
|
||||
|
||||
$related = $related->merge($byAuthor);
|
||||
}
|
||||
|
||||
if ($related->count() < 3) {
|
||||
$newest = Story::published()
|
||||
->with('author', 'tags')
|
||||
->where('id', '!=', $story->id)
|
||||
->whereNotIn('id', $related->pluck('id'))
|
||||
->orderByDesc('published_at')
|
||||
->limit(6 - $related->count())
|
||||
->get();
|
||||
|
||||
$related = $related->merge($newest);
|
||||
}
|
||||
|
||||
return $related->take(6);
|
||||
});
|
||||
|
||||
return view('web.stories.show', [
|
||||
'story' => $story,
|
||||
'related' => $related,
|
||||
'page_title' => $story->title . ' — Skinbase Stories',
|
||||
'page_meta_description' => $story->meta_excerpt,
|
||||
'page_canonical' => $story->url,
|
||||
'page_robots' => 'index,follow',
|
||||
'breadcrumbs' => collect([
|
||||
(object) ['name' => 'Stories', 'url' => '/stories'],
|
||||
(object) ['name' => $story->title, 'url' => $story->url],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Models\ContentType;
|
||||
use App\Models\Tag;
|
||||
use App\Services\ArtworkSearchService;
|
||||
use App\Services\EarlyGrowth\GridFiller;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
@@ -60,11 +61,10 @@ final class TagController extends Controller
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$artworks = $this->gridFiller->fill($artworks, 0, $page);
|
||||
|
||||
// Eager-load relations needed by the artwork-card component.
|
||||
// Scout returns bare Eloquent models; without this, each card triggers N+1 queries.
|
||||
$artworks->getCollection()->loadMissing(['user.profile']);
|
||||
// Eager-load relations used by the gallery presenter and thumbnails.
|
||||
$artworks->getCollection()->each(fn($m) => $m->loadMissing(['user.profile', 'categories']));
|
||||
|
||||
// Sidebar: content type links (same as browse gallery)
|
||||
// Sidebar: main content type links (same as browse gallery)
|
||||
$mainCategories = ContentType::orderBy('id')->get(['name', 'slug'])
|
||||
->map(fn ($type) => (object) [
|
||||
'id' => $type->id,
|
||||
@@ -73,15 +73,76 @@ final class TagController extends Controller
|
||||
'url' => '/' . strtolower($type->slug),
|
||||
]);
|
||||
|
||||
return view('tags.show', [
|
||||
'tag' => $tag,
|
||||
'artworks' => $artworks,
|
||||
'sort' => $sort,
|
||||
'ogImage' => null,
|
||||
'page_title' => 'Artworks tagged "' . $tag->name . '" — Skinbase',
|
||||
'page_meta_description' => 'Browse all Skinbase artworks tagged "' . $tag->name . '". Discover photography, wallpapers and skins.',
|
||||
'page_canonical' => route('tags.show', $tag->slug),
|
||||
'page_robots' => 'index,follow',
|
||||
// Map artworks into the lightweight shape expected by the gallery React component.
|
||||
$galleryCollection = $artworks->getCollection()->map(function ($a) {
|
||||
$primaryCategory = $a->categories->sortBy('sort_order')->first();
|
||||
$present = ThumbnailPresenter::present($a, 'md');
|
||||
$avatarUrl = \App\Support\AvatarUrl::forUser((int) ($a->user_id ?? 0), $a->user?->profile?->avatar_hash ?? null, 64);
|
||||
|
||||
return (object) [
|
||||
'id' => $a->id,
|
||||
'name' => $a->title ?? ($a->name ?? null),
|
||||
'category_name' => $primaryCategory->name ?? '',
|
||||
'category_slug' => $primaryCategory->slug ?? '',
|
||||
'thumb_url' => $present['url'] ?? ($a->thumbUrl('md') ?? null),
|
||||
'thumb_srcset' => $present['srcset'] ?? null,
|
||||
'uname' => $a->user?->name ?? '',
|
||||
'username' => $a->user?->username ?? '',
|
||||
'avatar_url' => $avatarUrl,
|
||||
'published_at' => $a->published_at ?? null,
|
||||
'width' => $a->width ?? null,
|
||||
'height' => $a->height ?? null,
|
||||
'slug' => $a->slug ?? null,
|
||||
];
|
||||
})->values();
|
||||
|
||||
// Replace paginator collection with the gallery-shaped collection so
|
||||
// the gallery.index blade will generate the expected JSON payload.
|
||||
if (method_exists($artworks, 'setCollection')) {
|
||||
$artworks->setCollection($galleryCollection);
|
||||
}
|
||||
|
||||
// Determine gallery sort mapping so the gallery UI highlights the right tab.
|
||||
$sortMapToGallery = [
|
||||
'popular' => 'trending',
|
||||
'latest' => 'latest',
|
||||
'likes' => 'top-rated',
|
||||
'downloads' => 'downloaded',
|
||||
];
|
||||
$gallerySort = $sortMapToGallery[$sort] ?? 'trending';
|
||||
|
||||
// Build simple pagination SEO links
|
||||
$prev = method_exists($artworks, 'previousPageUrl') ? $artworks->previousPageUrl() : null;
|
||||
$next = method_exists($artworks, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
|
||||
|
||||
return view('gallery.index', [
|
||||
'gallery_type' => 'tag',
|
||||
'mainCategories' => $mainCategories,
|
||||
'subcategories' => collect(),
|
||||
'contentType' => null,
|
||||
'category' => null,
|
||||
'artworks' => $artworks,
|
||||
'current_sort' => $gallerySort,
|
||||
'sort_options' => [
|
||||
['value' => 'trending', 'label' => '🔥 Trending'],
|
||||
['value' => 'fresh', 'label' => '🆕 New & Hot'],
|
||||
['value' => 'top-rated', 'label' => '⭐ Top Rated'],
|
||||
['value' => 'latest', 'label' => '🕐 Latest'],
|
||||
],
|
||||
'hero_title' => $tag->name,
|
||||
'hero_description' => 'Artworks tagged "' . $tag->name . '"',
|
||||
'breadcrumbs' => collect([
|
||||
(object) ['name' => 'Home', 'url' => '/'],
|
||||
(object) ['name' => 'Tags', 'url' => route('tags.index')],
|
||||
(object) ['name' => $tag->name, 'url' => route('tags.show', $tag->slug)],
|
||||
]),
|
||||
'page_title' => 'Artworks tagged "' . $tag->name . '" — Skinbase',
|
||||
'page_meta_description' => 'Browse all Skinbase artworks tagged "' . $tag->name . '".',
|
||||
'page_meta_keywords' => $tag->slug . ', skinbase, artworks, tag',
|
||||
'page_canonical' => route('tags.show', $tag->slug),
|
||||
'page_rel_prev' => $prev,
|
||||
'page_rel_next' => $next,
|
||||
'page_robots' => 'index,follow',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user