Wire admin studio SSR and search infrastructure

This commit is contained in:
2026-05-01 11:46:06 +02:00
parent 257b0dbef6
commit 18cea8b0f0
329 changed files with 197465 additions and 2741 deletions

View File

@@ -23,6 +23,58 @@
'my' => 'My Activity',
default => 'All Activity',
};
$serverActivities = $props['initialActivities'] ?? [];
$serverMeta = $props['initialMeta'] ?? [];
$serverFilter = $props['initialFilter'] ?? 'all';
$serverUserId = $props['initialUserId'] ?? null;
$serverIsAuthenticated = (bool) ($props['isAuthenticated'] ?? false);
$serverResultsLabel = ((int) ($serverMeta['total'] ?? count($serverActivities))) > 0
? number_format((int) ($serverMeta['total'] ?? count($serverActivities))) . ' events'
: 'No recent activity';
$serverFilterTabs = [
['key' => 'all', 'label' => 'All Activity', 'auth_required' => false],
['key' => 'comments', 'label' => 'Comments', 'auth_required' => false],
['key' => 'replies', 'label' => 'Replies', 'auth_required' => false],
['key' => 'following', 'label' => 'Following', 'auth_required' => true],
['key' => 'my', 'label' => 'My Activity', 'auth_required' => true],
];
$buildFilterUrl = static function (string $filterKey, ?int $userId): string {
return route('community.activity', array_filter([
'filter' => $filterKey !== 'all' ? $filterKey : null,
'user_id' => $userId,
], static fn (mixed $value): bool => $value !== null && $value !== ''));
};
$describeActivity = static function (array $activity): array {
$type = (string) ($activity['type'] ?? 'activity');
$artworkTitle = (string) data_get($activity, 'artwork.title', 'an artwork');
$artworkUrl = data_get($activity, 'artwork.url');
$storyTitle = (string) data_get($activity, 'story.title', 'a story');
$storyUrl = data_get($activity, 'story.url');
$targetUsername = (string) (data_get($activity, 'target_user.username') ?: data_get($activity, 'target_user.name', 'another creator'));
$targetUrl = data_get($activity, 'target_user.profile_url');
$mentionedUsername = (string) (data_get($activity, 'mentioned_user.username') ?: data_get($activity, 'mentioned_user.name', 'someone'));
$mentionedUrl = data_get($activity, 'mentioned_user.profile_url');
$commentAuthor = (string) (data_get($activity, 'comment.author.name') ?: data_get($activity, 'comment.author.username', 'a creator'));
$commentAuthorUrl = data_get($activity, 'comment.author.profile_url');
$reactionLabel = trim((string) data_get($activity, 'reaction.emoji', '')) . ' ' . (string) data_get($activity, 'reaction.label', 'Like');
return match ($type) {
'upload' => $storyUrl || data_get($activity, 'story.title')
? ['verb' => 'published', 'subject' => $storyTitle, 'subject_url' => $storyUrl, 'context' => null, 'context_url' => null]
: ['verb' => 'published', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'favorite' => ['verb' => 'favorited', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'follow' => ['verb' => 'followed', 'subject' => '@' . ltrim($targetUsername, '@'), 'subject_url' => $targetUrl, 'context' => null, 'context_url' => null],
'award' => ['verb' => 'awarded', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'story_like' => ['verb' => 'liked', 'subject' => $storyTitle, 'subject_url' => $storyUrl, 'context' => null, 'context_url' => null],
'story_comment' => ['verb' => 'commented on', 'subject' => $storyTitle, 'subject_url' => $storyUrl, 'context' => null, 'context_url' => null],
'comment' => ['verb' => 'commented on', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'reply' => ['verb' => 'replied on', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'reaction' => ['verb' => 'reacted ' . trim($reactionLabel), 'subject' => $commentAuthor, 'subject_url' => $commentAuthorUrl, 'context' => $artworkTitle, 'context_url' => $artworkUrl],
'mention' => ['verb' => 'mentioned', 'subject' => '@' . ltrim($mentionedUsername, '@'), 'subject_url' => $mentionedUrl, 'context' => $artworkTitle, 'context_url' => $artworkUrl],
default => ['verb' => 'shared new activity on', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
};
};
@endphp
@section('content')
@@ -58,15 +110,136 @@
<div id="community-activity-root" class="min-h-[480px]">
<div class="mx-auto max-w-6xl px-6 pt-8 pb-20 md:px-10">
<div class="mb-6 rounded-[28px] border border-white/[0.06] bg-white/[0.025] p-5 shadow-[0_18px_45px_rgba(0,0,0,0.22)]">
<div class="h-3 w-40 animate-pulse rounded bg-white/[0.08]"></div>
<div class="mt-3 h-3 w-2/3 animate-pulse rounded bg-white/[0.06]"></div>
<div class="mt-5 flex gap-2">
<div class="h-10 w-28 animate-pulse rounded-full bg-white/[0.06]"></div>
<div class="h-10 w-24 animate-pulse rounded-full bg-white/[0.05]"></div>
<div class="h-10 w-24 animate-pulse rounded-full bg-white/[0.05]"></div>
<div class="mb-6 flex flex-col gap-4 rounded-[28px] border border-white/[0.06] bg-[linear-gradient(180deg,rgba(10,16,26,0.92),rgba(6,10,18,0.88))] p-5 shadow-[0_20px_60px_rgba(0,0,0,0.25)]">
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-white/35">Live community pulse</p>
<p class="mt-2 max-w-2xl text-sm leading-6 text-white/55">
Comments, replies, reactions, and mentions from across Skinbase in one scrolling Nova feed.
</p>
</div>
<div class="text-sm font-medium text-white/45">{{ $serverResultsLabel }}</div>
</div>
<div class="flex flex-wrap items-center gap-2">
@foreach ($serverFilterTabs as $tab)
@php
$isDisabled = $tab['auth_required'] && ! $serverIsAuthenticated;
$isActive = $serverFilter === $tab['key'];
@endphp
@if ($isDisabled)
<span class="cursor-not-allowed rounded-full border border-white/[0.06] bg-white/[0.03] px-4 py-2 text-sm font-medium text-white/35 opacity-60" aria-disabled="true">
{{ $tab['label'] }}
</span>
@else
<a
href="{{ $buildFilterUrl($tab['key'], $serverUserId) }}"
class="rounded-full border px-4 py-2 text-sm font-medium transition-all {{ $isActive ? 'border-sky-400/30 bg-sky-500/14 text-sky-200 shadow-[0_0_0_1px_rgba(56,189,248,0.08)]' : 'border-white/[0.06] bg-white/[0.03] text-white/55 hover:border-white/15 hover:bg-white/[0.05] hover:text-white/85' }}"
>
{{ $tab['label'] }}
</a>
@endif
@endforeach
</div>
</div>
@if (count($serverActivities) === 0)
<div class="rounded-[28px] border border-white/[0.06] bg-white/[0.025] px-6 py-16 text-center">
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-white/[0.06] bg-white/[0.03] text-white/35">
<i class="fa-solid fa-wave-square text-xl"></i>
</div>
<h3 class="text-lg font-semibold text-white/80">No activity yet</h3>
<p class="mx-auto mt-2 max-w-md text-sm leading-6 text-white/45">
When creators and members interact around artworks, their activity will appear here.
</p>
</div>
@else
<div class="space-y-4">
@foreach ($serverActivities as $activity)
@php
$activityUser = data_get($activity, 'user', []);
$activityArtwork = data_get($activity, 'artwork', []);
$activityStory = data_get($activity, 'story', []);
$activityCommentBody = trim((string) data_get($activity, 'comment.body', ''));
$activityHeadline = $describeActivity($activity);
$activityAvatarUrl = data_get($activityUser, 'avatar_url') ?: '/images/avatar_default.webp';
$activityProfileUrl = data_get($activityUser, 'profile_url');
$activityName = data_get($activityUser, 'name') ?: data_get($activityUser, 'username', 'Skinbase creator');
$activityUsername = data_get($activityUser, 'username');
$activityPreviewUrl = data_get($activityArtwork, 'thumb_url') ?: data_get($activityStory, 'cover_url');
$activityPreviewAlt = data_get($activityArtwork, 'title') ?: data_get($activityStory, 'title') ?: 'Activity preview';
$activityPreviewLink = data_get($activityArtwork, 'url') ?: data_get($activityStory, 'url');
@endphp
<article class="rounded-[28px] border border-white/[0.06] bg-[linear-gradient(180deg,rgba(11,16,26,0.96),rgba(7,11,19,0.92))] p-4 shadow-[0_18px_45px_rgba(0,0,0,0.28)] backdrop-blur-xl sm:p-5">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start">
<div class="sm:w-[220px] sm:shrink-0">
<div class="flex items-start gap-3">
<a href="{{ $activityProfileUrl ?: '#' }}" class="shrink-0 {{ $activityProfileUrl ? '' : 'pointer-events-none' }}">
<img src="{{ $activityAvatarUrl }}" alt="{{ $activityName }}" class="h-11 w-11 rounded-2xl object-cover" loading="lazy">
</a>
<div class="min-w-0">
<p class="text-sm font-semibold text-white">
@if ($activityProfileUrl)
<a href="{{ $activityProfileUrl }}" class="hover:text-sky-200">{{ $activityName }}</a>
@else
{{ $activityName }}
@endif
</p>
@if ($activityUsername)
<p class="text-xs uppercase tracking-[0.18em] text-white/35">@{{ $activityUsername }}</p>
@endif
<p class="mt-2 text-[11px] uppercase tracking-[0.18em] text-white/25">{{ data_get($activity, 'time_ago', '') }}</p>
</div>
</div>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm leading-6 text-white/70">
<span class="font-medium text-white">{{ $activityHeadline['verb'] }}</span>
@if (!empty($activityHeadline['subject']))
<span> </span>
@if (!empty($activityHeadline['subject_url']))
<a href="{{ $activityHeadline['subject_url'] }}" class="text-sky-300 hover:text-sky-200">{{ $activityHeadline['subject'] }}</a>
@else
<span class="text-white">{{ $activityHeadline['subject'] }}</span>
@endif
@endif
@if (!empty($activityHeadline['context']))
<span> on </span>
@if (!empty($activityHeadline['context_url']))
<a href="{{ $activityHeadline['context_url'] }}" class="text-sky-300 hover:text-sky-200">{{ $activityHeadline['context'] }}</a>
@else
<span class="text-white">{{ $activityHeadline['context'] }}</span>
@endif
@endif
</p>
@if ($activityCommentBody !== '')
<div class="mt-3 rounded-2xl border border-white/[0.06] bg-white/[0.025] px-4 py-3">
<p class="whitespace-pre-line break-words text-sm leading-6 text-white/80">{{ \Illuminate\Support\Str::limit($activityCommentBody, 240) }}</p>
</div>
@endif
@if (($activity['type'] ?? null) === 'mention' && data_get($activity, 'mentioned_user.username'))
<div class="mt-3 inline-flex items-center gap-2 rounded-full border border-sky-400/20 bg-sky-500/10 px-3 py-1 text-xs text-sky-200">
<i class="fa-solid fa-at"></i>
Mentioned @{{ data_get($activity, 'mentioned_user.username') }}
</div>
@endif
</div>
@if ($activityPreviewUrl)
<div class="sm:ml-auto sm:w-[120px] sm:shrink-0">
<a href="{{ $activityPreviewLink ?: '#' }}" class="block overflow-hidden rounded-2xl border border-white/[0.06] bg-white/[0.03] {{ $activityPreviewLink ? '' : 'pointer-events-none' }}">
<img src="{{ $activityPreviewUrl }}" alt="{{ $activityPreviewAlt }}" class="h-[132px] w-full object-cover" loading="lazy">
</a>
</div>
@endif
</div>
</article>
@endforeach
</div>
@endif
</div>
</div>