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:
2026-03-21 11:02:22 +01:00
parent 29c3ff8572
commit 979e011257
55 changed files with 2576 additions and 1923 deletions

View File

@@ -1,27 +1,288 @@
@extends('layouts.nova')
@section('content')
<div class="container mx-auto py-8 max-w-3xl">
<h1 class="text-2xl font-semibold mb-6">My Followers</h1>
<div class="container-fluid legacy-page">
<div class="pt-0">
<div class="mx-auto w-full">
<div class="relative min-h-[calc(120vh-64px)] md:min-h-[calc(100vh-64px)]">
<main class="w-full">
<x-nova-page-header
section="Dashboard"
title="People Following Me"
icon="fa-users"
:breadcrumbs="collect([
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
(object) ['name' => 'Followers', 'url' => route('dashboard.followers')],
])"
description="A clearer view of who follows you, who you follow back, and who still needs a response."
actionsClass="lg:pt-8"
>
<x-slot name="actions">
<div class="flex flex-wrap items-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-user-check text-xs"></i>
People I follow
</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>
@if($followers->isEmpty())
<p class="text-sm text-gray-500">You have no followers yet.</p>
@else
<div class="space-y-3">
@foreach($followers as $f)
<a href="{{ $f->profile_url }}" class="flex items-center gap-4 p-3 rounded-lg hover:bg-white/5 transition">
<img src="{{ $f->avatar_url }}" alt="{{ $f->uname }}" class="w-10 h-10 rounded-full object-cover">
<div class="flex-1 min-w-0">
<div class="font-medium truncate">{{ $f->uname }}</div>
<div class="text-xs text-gray-500">{{ $f->uploads }} uploads &middot; followed {{ \Carbon\Carbon::parse($f->followed_at)->diffForHumans() }}</div>
</div>
</a>
@endforeach
</div>
<section class="px-6 pb-16 pt-8 md:px-10">
@php
$newestFollower = $followers->getCollection()->first();
$newestFollowerName = $newestFollower ? ($newestFollower->name ?: $newestFollower->uname) : null;
$latestFollowedAt = $newestFollower && !empty($newestFollower->followed_at)
? \Carbon\Carbon::parse($newestFollower->followed_at)->diffForHumans()
: null;
@endphp
<div class="mt-6">
{{ $followers->links() }}
<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">Total followers</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['total_followers']) }}</p>
<p class="mt-2 text-xs text-white/40">People currently following your profile</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">Followed back</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['following_back']) }}</p>
<p class="mt-2 text-xs text-white/40">Followers you also follow</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">Not followed back</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['not_followed']) }}</p>
<p class="mt-2 text-xs text-white/40">Followers still waiting on your 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">Newest follower</p>
<p class="mt-2 truncate text-xl font-semibold text-white">{{ $newestFollowerName ?? '—' }}</p>
<p class="mt-2 text-xs text-sky-50/60">{{ $latestFollowedAt ?? 'No recent follower 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
$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' => 'All followers'],
['value' => 'following-back', 'label' => 'I follow back'],
['value' => 'not-followed', 'label' => 'Not followed back'],
];
@endphp
<form method="GET" action="{{ route('dashboard.followers') }}" 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 follower</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.followers') }}" 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>
</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($followers->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'] === 'following-back' ? 'Following back only' : 'Not followed back' }}
</span>
@endif
</div>
</div>
@if($followers->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-users text-lg"></i>
</div>
<h2 class="text-xl font-semibold text-white">No followers 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 and activity to grow your audience.
</p>
<div class="mt-6 flex flex-wrap items-center justify-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-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>
Explore creators
</a>
</div>
</div>
@else
<div class="grid gap-5 lg:grid-cols-2 2xl:grid-cols-3">
@foreach($followers as $f)
@php
$displayName = $f->name ?: $f->uname;
$profileUsername = strtolower((string) ($f->username ?? ''));
@endphp
<article 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-emerald-400/20 bg-emerald-400/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-wide text-emerald-200">
Follows you
</span>
@if($f->is_following_back)
<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">
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">
Followed you {{ !empty($f->followed_at) ? \Carbon\Carbon::parse($f->followed_at)->diffForHumans() : 'recently' }}
</div>
</div>
</div>
</a>
<div class="shrink-0">
@if(!empty($profileUsername))
<div x-data="{
following: {{ $f->is_following_back ? 'true' : 'false' }},
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;
}
}">
<button @click="toggle"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
:disabled="loading"
class="inline-flex items-center gap-2 rounded-xl border px-3.5 py-2 text-sm font-medium transition-all"
:class="following
? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-100 hover:border-rose-400/30 hover:bg-rose-400/10 hover:text-rose-100'
: 'border-sky-400/30 bg-sky-400/10 text-sky-100 hover:bg-sky-400/15 hover:border-sky-300/40'">
<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 back') : 'Follow back'"></span>
</button>
</div>
@endif
</div>
</div>
<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>
<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">{{ 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->is_following_back ? 'text-emerald-200' : 'text-amber-200' }}">
{{ $f->is_following_back ? 'Mutual follow' : 'Follower only' }}
</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->is_following_back && !empty($f->followed_back_at)
? 'You followed back ' . \Carbon\Carbon::parse($f->followed_back_at)->diffForHumans()
: 'Not followed 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">
{{ $followers->links() }}
</div>
@endif
</section>
</main>
</div>
</div>
@endif
</div>
</div>
@endsection