Refactor dashboard and upload flows
Remove dead admin UI code, redesign dashboard followers/following and upload experiences, and add schema audit tooling with repair migrations for forum and upload drift.
This commit is contained in:
@@ -18,84 +18,262 @@
|
||||
actionsClass="lg:pt-8"
|
||||
>
|
||||
<x-slot name="actions">
|
||||
<a href="{{ route('discover.trending') }}"
|
||||
class="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium border border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-compass text-xs"></i>
|
||||
Discover creators
|
||||
</a>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<a href="{{ route('dashboard.followers') }}"
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
|
||||
<i class="fa-solid fa-users text-xs"></i>
|
||||
My followers
|
||||
</a>
|
||||
<a href="{{ route('discover.trending') }}"
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-400/10 px-4 py-2 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
|
||||
<i class="fa-solid fa-compass text-xs"></i>
|
||||
Discover creators
|
||||
</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-nova-page-header>
|
||||
|
||||
<section class="px-6 pb-16 pt-8 md:px-10">
|
||||
@php
|
||||
$firstFollow = $following->getCollection()->first();
|
||||
$latestFollowedAt = $firstFollow && !empty($firstFollow->followed_at)
|
||||
? \Carbon\Carbon::parse($firstFollow->followed_at)->diffForHumans()
|
||||
: null;
|
||||
$latestFollowedName = $firstFollow ? ($firstFollow->name ?: $firstFollow->uname) : null;
|
||||
@endphp
|
||||
|
||||
@if($following->isEmpty())
|
||||
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
|
||||
<p class="text-white/40 text-sm">You are not following anyone yet.</p>
|
||||
<a href="{{ route('discover.trending') }}" class="mt-4 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium border border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-compass text-xs"></i>
|
||||
Start following creators
|
||||
</a>
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
|
||||
<p class="text-xs uppercase tracking-widest text-white/35">Following</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['total_following']) }}</p>
|
||||
<p class="mt-2 text-xs text-white/40">People you currently follow</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
|
||||
<p class="text-xs uppercase tracking-widest text-white/35">Mutual follows</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['mutual']) }}</p>
|
||||
<p class="mt-2 text-xs text-white/40">People who follow you back</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
|
||||
<p class="text-xs uppercase tracking-widest text-white/35">One-way follows</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['one_way']) }}</p>
|
||||
<p class="mt-2 text-xs text-white/40">People you follow who do not follow back</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-sky-400/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.12),rgba(255,255,255,0.03))] p-5 shadow-[0_16px_60px_rgba(14,165,233,0.08)]">
|
||||
<p class="text-xs uppercase tracking-widest text-sky-100/60">Latest followed</p>
|
||||
<p class="mt-2 truncate text-xl font-semibold text-white">{{ $latestFollowedName ?? '—' }}</p>
|
||||
<p class="mt-2 text-xs text-sky-50/60">{{ $latestFollowedAt ?? 'No recent follow activity' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 rounded-2xl border border-white/[0.06] bg-white/[0.03] p-4 shadow-[0_16px_60px_rgba(0,0,0,0.12)]">
|
||||
@php
|
||||
$firstFollow = $following->getCollection()->first();
|
||||
$latestFollowedAt = $firstFollow && !empty($firstFollow->followed_at)
|
||||
? \Carbon\Carbon::parse($firstFollow->followed_at)->diffForHumans()
|
||||
: null;
|
||||
$sortOptions = [
|
||||
['value' => 'recent', 'label' => 'Most recent'],
|
||||
['value' => 'oldest', 'label' => 'Oldest first'],
|
||||
['value' => 'name', 'label' => 'Name A-Z'],
|
||||
['value' => 'uploads', 'label' => 'Most uploads'],
|
||||
['value' => 'followers', 'label' => 'Most followers'],
|
||||
];
|
||||
|
||||
$relationshipOptions = [
|
||||
['value' => 'all', 'label' => 'Everyone I follow'],
|
||||
['value' => 'mutual', 'label' => 'Mutual follows'],
|
||||
['value' => 'one-way', 'label' => 'Not following me back'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<div class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-4">
|
||||
<p class="text-xs uppercase tracking-widest text-white/35">Following</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($following->total()) }}</p>
|
||||
<form method="GET" action="{{ route('dashboard.following') }}" class="grid gap-4 lg:grid-cols-[minmax(0,1.35fr)_220px_220px_auto] lg:items-end">
|
||||
<label class="block">
|
||||
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Search creator</span>
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-magnifying-glass pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-xs text-white/30"></i>
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="{{ $filters['q'] }}"
|
||||
placeholder="Search by username or display name"
|
||||
class="w-full rounded-xl border border-white/[0.08] bg-black/20 py-3 pl-10 pr-4 text-sm text-white placeholder:text-white/30 focus:border-sky-400/40 focus:outline-none"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Sort by</span>
|
||||
<x-dashboard.filter-select
|
||||
name="sort"
|
||||
:value="$filters['sort']"
|
||||
:options="$sortOptions"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Relationship</span>
|
||||
<x-dashboard.filter-select
|
||||
name="relationship"
|
||||
:value="$filters['relationship']"
|
||||
:options="$relationshipOptions"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-wrap gap-3 lg:justify-end">
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-xl border border-sky-400/30 bg-sky-400/10 px-4 py-3 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
|
||||
<i class="fa-solid fa-sliders text-xs"></i>
|
||||
Apply
|
||||
</button>
|
||||
@if($filters['q'] !== '' || $filters['sort'] !== 'recent' || $filters['relationship'] !== 'all')
|
||||
<a href="{{ route('dashboard.following') }}" class="inline-flex items-center gap-2 rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
|
||||
<i class="fa-solid fa-rotate-left text-xs"></i>
|
||||
Reset
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-4">
|
||||
<p class="text-xs uppercase tracking-widest text-white/35">On this page</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($following->count()) }}</p>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-white/50">{{ number_format($following->count()) }} visible on this page</span>
|
||||
@if($filters['q'] !== '')
|
||||
<span class="inline-flex items-center rounded-full border border-sky-400/20 bg-sky-400/10 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-sky-100/80">Search: {{ $filters['q'] }}</span>
|
||||
@endif
|
||||
@if($filters['relationship'] !== 'all')
|
||||
<span class="inline-flex items-center rounded-full border border-emerald-400/20 bg-emerald-400/10 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-emerald-100/80">
|
||||
{{ $filters['relationship'] === 'mutual' ? 'Mutual follows only' : 'Not following you back' }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($following->isEmpty())
|
||||
<div class="rounded-2xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center shadow-[0_20px_80px_rgba(0,0,0,0.18)]">
|
||||
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.04] text-white/60">
|
||||
<i class="fa-solid fa-user-group text-lg"></i>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-4 sm:col-span-2 xl:col-span-1">
|
||||
<p class="text-xs uppercase tracking-widest text-white/35">Last followed</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-white">{{ $latestFollowedAt ?? '—' }}</p>
|
||||
<h2 class="text-xl font-semibold text-white">No followed creators match these filters</h2>
|
||||
<p class="mx-auto mt-2 max-w-xl text-sm text-white/45">
|
||||
Try resetting the filters, or discover more creators to build a stronger network.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="{{ route('dashboard.following') }}" class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
|
||||
<i class="fa-solid fa-rotate-left text-xs"></i>
|
||||
Reset filters
|
||||
</a>
|
||||
<a href="{{ route('discover.trending') }}" class="inline-flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-400/10 px-4 py-2 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
|
||||
<i class="fa-solid fa-compass text-xs"></i>
|
||||
Discover creators
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/[0.06] overflow-hidden">
|
||||
<div class="grid grid-cols-[1fr_auto_auto] items-center gap-4 px-5 py-3 bg-white/[0.03] border-b border-white/[0.06]">
|
||||
<span class="text-xs font-semibold uppercase tracking-widest text-white/30">Creator</span>
|
||||
<span class="hidden sm:block text-xs font-semibold uppercase tracking-widest text-white/30 text-right">Stats</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-widest text-white/30 text-right">Followed</span>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-white/[0.04]">
|
||||
@foreach($following as $f)
|
||||
@php
|
||||
$displayName = $f->name ?: $f->uname;
|
||||
@endphp
|
||||
<a href="{{ $f->profile_url }}"
|
||||
class="grid grid-cols-[1fr_auto_auto] items-center gap-4 px-5 py-4 hover:bg-white/[0.03] transition-colors">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<img src="{{ $f->avatar_url }}"
|
||||
alt="{{ $displayName }}"
|
||||
class="w-11 h-11 rounded-full object-cover flex-shrink-0 ring-1 ring-white/[0.10]"
|
||||
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-semibold text-white/90">{{ $displayName }}</div>
|
||||
@if(!empty($f->username))
|
||||
<div class="truncate text-xs text-white/35">{{ '@' . $f->username }}</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="grid gap-5 lg:grid-cols-2 2xl:grid-cols-3">
|
||||
@foreach($following as $f)
|
||||
@php
|
||||
$displayName = $f->name ?: $f->uname;
|
||||
$profileUsername = strtolower((string) ($f->username ?? ''));
|
||||
@endphp
|
||||
<article
|
||||
x-data="{
|
||||
following: true,
|
||||
count: {{ (int) $f->followers_count }},
|
||||
loading: false,
|
||||
hovering: false,
|
||||
async toggle() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const r = await fetch('{{ route('profile.follow', ['username' => $profileUsername]) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const d = await r.json();
|
||||
if (r.ok) {
|
||||
this.following = d.following;
|
||||
this.count = d.follower_count;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
}"
|
||||
:class="following ? 'opacity-100' : 'opacity-50'"
|
||||
class="group overflow-hidden rounded-2xl border border-white/[0.06] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0.02))] shadow-[0_18px_70px_rgba(0,0,0,0.14)] transition-all hover:-translate-y-0.5 hover:border-white/[0.10] hover:shadow-[0_24px_90px_rgba(0,0,0,0.20)]">
|
||||
<div class="flex items-start justify-between gap-4 border-b border-white/[0.05] px-5 py-5">
|
||||
<a href="{{ $f->profile_url }}" class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<img src="{{ $f->avatar_url }}"
|
||||
alt="{{ $displayName }}"
|
||||
class="h-14 w-14 flex-shrink-0 rounded-2xl object-cover ring-1 ring-white/[0.10]"
|
||||
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'">
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h2 class="truncate text-base font-semibold text-white/95 group-hover:text-white">{{ $displayName }}</h2>
|
||||
<span class="inline-flex items-center rounded-full border border-sky-400/20 bg-sky-400/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-wide text-sky-100">
|
||||
You follow
|
||||
</span>
|
||||
@if($f->follows_you)
|
||||
<span class="inline-flex items-center rounded-full border border-emerald-400/20 bg-emerald-400/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-wide text-emerald-200">
|
||||
Mutual
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@if(!empty($f->username))
|
||||
<div class="mt-1 truncate text-xs text-white/35">{{ '@' . $f->username }}</div>
|
||||
@endif
|
||||
<div class="mt-2 text-xs text-white/45">
|
||||
You followed {{ !empty($f->followed_at) ? \Carbon\Carbon::parse($f->followed_at)->diffForHumans() : 'recently' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="hidden sm:block text-right text-xs text-white/55">
|
||||
{{ number_format((int) $f->uploads) }} uploads · {{ number_format((int) $f->followers_count) }} followers
|
||||
<div class="shrink-0">
|
||||
@if(!empty($profileUsername))
|
||||
<div>
|
||||
<button @click="toggle"
|
||||
@mouseenter="hovering = true"
|
||||
@mouseleave="hovering = false"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center gap-2 rounded-xl border border-emerald-400/25 bg-emerald-400/10 px-3.5 py-2 text-sm font-medium text-emerald-100 transition-all hover:border-rose-400/30 hover:bg-rose-400/10 hover:text-rose-100"
|
||||
:class="!following ? 'border-white/[0.08] bg-white/[0.04] text-white/60' : ''">
|
||||
<i class="fa-solid fa-fw text-xs"
|
||||
:class="loading ? 'fa-circle-notch fa-spin' : (following ? (hovering ? 'fa-user-minus' : 'fa-user-check') : 'fa-user-plus')"></i>
|
||||
<span x-text="following ? (hovering ? 'Unfollow' : 'Following') : 'Follow back'"></span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right text-xs text-white/45 whitespace-nowrap">
|
||||
{{ !empty($f->followed_at) ? \Carbon\Carbon::parse($f->followed_at)->diffForHumans() : '—' }}
|
||||
<div class="grid gap-3 px-5 py-4 sm:grid-cols-3">
|
||||
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
|
||||
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Uploads</p>
|
||||
<p class="mt-1 text-lg font-semibold text-white">{{ number_format((int) $f->uploads) }}</p>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
|
||||
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Followers</p>
|
||||
<p class="mt-1 text-lg font-semibold text-white" x-text="typeof count !== 'undefined' ? Number(count).toLocaleString() : '{{ number_format((int) $f->followers_count) }}'"></p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
|
||||
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Relationship</p>
|
||||
<p class="mt-1 text-sm font-semibold {{ $f->follows_you ? 'text-emerald-200' : 'text-amber-200' }}">
|
||||
{{ $f->follows_you ? 'Mutual follow' : 'You follow them' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3 border-t border-white/[0.05] px-5 py-4 text-xs text-white/45">
|
||||
<span>
|
||||
{{ $f->follows_you && !empty($f->follows_you_at)
|
||||
? 'They followed you ' . \Carbon\Carbon::parse($f->follows_you_at)->diffForHumans()
|
||||
: 'They do not follow you back yet' }}
|
||||
</span>
|
||||
<a href="{{ $f->profile_url }}" class="inline-flex items-center gap-2 font-medium text-white/70 transition-colors hover:text-white">
|
||||
View profile
|
||||
<i class="fa-solid fa-arrow-right text-[10px]"></i>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-center">
|
||||
|
||||
Reference in New Issue
Block a user