login update

This commit is contained in:
2026-03-05 11:24:37 +01:00
parent 5a33ca55a1
commit f6772f673b
67 changed files with 10640 additions and 116 deletions

View File

@@ -594,7 +594,7 @@ final class HomepageService
$authorName = $artwork->user?->name ?? 'Artist';
$authorUsername = $artwork->user?->username ?? '';
$avatarHash = $artwork->user?->profile?->avatar_hash ?? null;
$authorAvatar = AvatarUrl::forUser((int) $authorId, $avatarHash, 40);
$authorAvatar = AvatarUrl::forUser((int) $authorId, $avatarHash, 64);
return [
'id' => $artwork->id,

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace App\Services\RSS;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
/**
* RSSFeedBuilder
*
* Responsible for:
* - normalising feed items from Eloquent collections
* - enforcing feed limits (max 20 items)
* - rendering RSS 2.0 XML via the rss.channel Blade template
* - returning a properly typed HTTP Response
*/
final class RSSFeedBuilder
{
/** Hard item limit per feed (spec §7). */
public const FEED_LIMIT = 20;
// ── Public builders ───────────────────────────────────────────────────────
/**
* Build an RSS 2.0 Response from an Artwork Eloquent collection.
* Artworks must have 'user' and 'categories' relations preloaded.
*/
public function buildFromArtworks(
string $channelTitle,
string $channelDescription,
string $feedUrl,
Collection $artworks,
): Response {
$items = $artworks->take(self::FEED_LIMIT)->map(fn ($a) => $this->artworkToItem($a));
return $this->buildResponse($channelTitle, $channelDescription, url('/'), $feedUrl, $items);
}
/**
* Build an RSS 2.0 Response from a BlogPost Eloquent collection.
* Posts must have 'author' relation preloaded.
*/
public function buildFromBlogPosts(
string $channelTitle,
string $channelDescription,
string $feedUrl,
Collection $posts,
): Response {
$items = $posts->take(self::FEED_LIMIT)->map(fn ($p) => $this->blogPostToItem($p));
return $this->buildResponse($channelTitle, $channelDescription, url('/blog'), $feedUrl, $items);
}
// ── Private helpers ───────────────────────────────────────────────────────
private function buildResponse(
string $channelTitle,
string $channelDescription,
string $channelLink,
string $feedUrl,
Collection $items,
): Response {
$xml = view('rss.channel', [
'channelTitle' => trim($channelTitle) . ' — Skinbase',
'channelDescription' => $channelDescription,
'channelLink' => $channelLink,
'feedUrl' => $feedUrl,
'items' => $items,
'buildDate' => now()->toRfc2822String(),
])->render();
return response($xml, 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8',
'Cache-Control' => 'public, max-age=300',
]);
}
/** Convert an Artwork model to an RSS item array. */
private function artworkToItem(object $artwork): array
{
$link = url('/art/' . $artwork->id . '/' . ($artwork->slug ?? ''));
$thumb = method_exists($artwork, 'thumbUrl') ? $artwork->thumbUrl('sm') : null;
// Primary category from eagerly loaded relation (avoid N+1)
$primaryCategory = ($artwork->relationLoaded('categories'))
? $artwork->categories->first()
: null;
// Build HTML description embedded in CDATA
$descParts = [];
if ($thumb) {
$descParts[] = '<img src="' . htmlspecialchars($thumb, ENT_XML1) . '" '
. 'alt="' . htmlspecialchars((string) $artwork->title, ENT_XML1) . '" />';
}
if (!empty($artwork->description)) {
$descParts[] = '<p>' . htmlspecialchars(strip_tags((string) $artwork->description), ENT_XML1) . '</p>';
}
return [
'title' => (string) $artwork->title,
'link' => $link,
'guid' => $link,
'description' => implode('', $descParts),
'pubDate' => $artwork->published_at?->toRfc2822String(),
'author' => $artwork->user?->username ?? 'Unknown',
'category' => $primaryCategory?->name,
'enclosure' => $thumb ? [
'url' => $thumb,
'length' => 0,
'type' => 'image/jpeg',
] : null,
];
}
/** Convert a BlogPost model to an RSS item array. */
private function blogPostToItem(object $post): array
{
$link = url('/blog/' . $post->slug);
$excerpt = $post->excerpt ?? strip_tags((string) ($post->body ?? ''));
return [
'title' => (string) $post->title,
'link' => $link,
'guid' => $link,
'description' => $excerpt,
'pubDate' => $post->published_at?->toRfc2822String(),
'author' => $post->author?->username ?? 'Skinbase',
'category' => null,
'enclosure' => !empty($post->featured_image) ? [
'url' => $post->featured_image,
'length' => 0,
'type' => 'image/jpeg',
] : null,
];
}
}