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:
@@ -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 · 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
|
||||
|
||||
Reference in New Issue
Block a user