257 lines
17 KiB
PHP
257 lines
17 KiB
PHP
@extends('layouts.nova')
|
|
|
|
@php
|
|
$page_title = $page_title ?? 'Community Activity';
|
|
$page_meta_description = 'Track comments, replies, reactions, and mentions from across the Skinbase community in one live feed.';
|
|
$page_canonical = route('community.activity', array_filter([
|
|
'filter' => ($initialFilter ?? null) && ($initialFilter ?? 'all') !== 'all' ? $initialFilter : null,
|
|
'user_id' => $initialUserId ?? null,
|
|
], fn (mixed $value): bool => $value !== null && $value !== ''));
|
|
$useUnifiedSeo = true;
|
|
$headerBreadcrumbs = collect([
|
|
(object) ['name' => $page_title, 'url' => $page_canonical],
|
|
]);
|
|
$breadcrumbs = $headerBreadcrumbs;
|
|
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
|
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
|
)->build();
|
|
$communityActivityManifestPath = public_path('build/manifest.json');
|
|
$communityActivityManifest = is_file($communityActivityManifestPath)
|
|
? json_decode((string) file_get_contents($communityActivityManifestPath), true)
|
|
: null;
|
|
$communityActivityViteReady = is_array($communityActivityManifest)
|
|
&& array_key_exists('resources/js/Pages/Community/CommunityActivityPage.jsx', $communityActivityManifest);
|
|
|
|
$initialFilterLabel = match (($initialFilter ?? 'all')) {
|
|
'comments' => 'Comments',
|
|
'replies' => 'Replies',
|
|
'following' => 'Following',
|
|
'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')
|
|
<x-nova-page-header
|
|
section="Community"
|
|
:title="$page_title ?? 'Community Activity'"
|
|
icon="fa-wave-square"
|
|
:breadcrumbs="$headerBreadcrumbs"
|
|
description="Track comments, replies, reactions, and mentions from across the Skinbase community in one live feed."
|
|
headerClass="pb-6"
|
|
>
|
|
<x-slot name="actions">
|
|
<div class="flex flex-wrap items-center gap-2 text-xs font-medium">
|
|
<span id="community-activity-filter-summary" class="inline-flex items-center gap-1.5 rounded-full border border-sky-400/20 bg-sky-500/10 px-3 py-1 text-sky-200">
|
|
<i class="fa-solid fa-filter"></i>
|
|
{{ $initialFilterLabel }}
|
|
</span>
|
|
@if (!empty($initialUserId))
|
|
<span id="community-activity-scope-summary" class="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-white/65">
|
|
<i class="fa-solid fa-user"></i>
|
|
User #{{ $initialUserId }}
|
|
</span>
|
|
@else
|
|
<span id="community-activity-scope-summary" class="hidden"></span>
|
|
@endif
|
|
</div>
|
|
</x-slot>
|
|
</x-nova-page-header>
|
|
|
|
<script id="community-activity-props" type="application/json">
|
|
{!! json_encode($props, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) !!}
|
|
</script>
|
|
|
|
<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 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>
|
|
|
|
@if ($communityActivityViteReady)
|
|
@vite(['resources/js/Pages/Community/CommunityActivityPage.jsx'])
|
|
@endif
|
|
|
|
@endsection
|