Save workspace changes
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto py-8">
|
||||
<h1 class="text-2xl font-semibold mb-1">My Awards</h1>
|
||||
<p class="text-sm text-soft mb-6">Artworks of yours that have received awards from the community.</p>
|
||||
|
||||
@if($artworks->isEmpty())
|
||||
<div class="flex flex-col items-center justify-center py-20 text-center">
|
||||
<i class="fa-solid fa-trophy text-4xl text-sb-muted mb-4"></i>
|
||||
<p class="text-soft">None of your artworks have received awards yet.</p>
|
||||
<a href="/browse" class="mt-4 text-sm text-accent hover:underline">Browse artworks for inspiration</a>
|
||||
</div>
|
||||
@else
|
||||
<section data-nova-gallery data-gallery-type="dashboard-awards">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" data-gallery-grid>
|
||||
@foreach($artworks as $art)
|
||||
<div class="relative gallery-item">
|
||||
<x-artwork-card :art="$art" />
|
||||
@php $stat = $art->awardStat @endphp
|
||||
@if($stat && ($stat->gold_count + $stat->silver_count + $stat->bronze_count) > 0)
|
||||
<div class="absolute left-2 top-2 z-40 flex gap-1 text-xs font-bold">
|
||||
@if($stat->gold_count) <span class="rounded px-1.5 py-0.5 bg-yellow-500/80 text-black">🥇 {{ $stat->gold_count }}</span> @endif
|
||||
@if($stat->silver_count) <span class="rounded px-1.5 py-0.5 bg-neutral-400/80 text-black">🥈 {{ $stat->silver_count }}</span> @endif
|
||||
@if($stat->bronze_count) <span class="rounded px-1.5 py-0.5 bg-amber-700/80 text-white">🥉 {{ $stat->bronze_count }}</span> @endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-6" data-gallery-pagination>{{ $artworks->links() }}</div>
|
||||
<div class="hidden" data-gallery-skeleton-template aria-hidden="true">
|
||||
<x-skeleton.artwork-card />
|
||||
</div>
|
||||
<div class="hidden mt-8" data-gallery-skeleton></div>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,244 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<x-nova-page-header
|
||||
section="Dashboard"
|
||||
title="Received Comments"
|
||||
icon="fa-comments"
|
||||
:breadcrumbs="collect([
|
||||
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
|
||||
(object) ['name' => 'Comments', 'url' => route('dashboard.comments.received')],
|
||||
])"
|
||||
description="A clean inbox for feedback on your artworks, with quick scanning, filtering, and direct artwork context."
|
||||
>
|
||||
<x-slot name="actions">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{{ route('community.activity') }}"
|
||||
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-wave-square text-xs"></i>
|
||||
Community activity
|
||||
</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-nova-page-header>
|
||||
|
||||
<div class="px-6 pb-16 pt-8 md:px-10">
|
||||
<div class="mb-6 flex flex-wrap items-center gap-2 rounded-2xl border border-white/[0.06] bg-white/[0.025] p-2 shadow-[0_12px_30px_rgba(0,0,0,0.14)]">
|
||||
<a href="{{ route('dashboard.comments.received') }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition {{ request()->routeIs('dashboard.comments.received') ? 'bg-sky-500/15 text-sky-200 ring-1 ring-sky-400/20' : 'text-white/60 hover:bg-white/[0.05] hover:text-white' }}">
|
||||
<i class="fa-solid fa-inbox"></i>
|
||||
Received
|
||||
</a>
|
||||
<a href="{{ route('messages.index') }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.05] hover:text-white">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
Messages
|
||||
</a>
|
||||
<a href="{{ route('dashboard.notifications') }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.05] hover:text-white">
|
||||
<i class="fa-solid fa-bell"></i>
|
||||
Notifications
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(($freshlyClearedCount ?? 0) > 0)
|
||||
<div class="mb-6 rounded-2xl border border-emerald-400/20 bg-emerald-500/10 px-5 py-4 text-sm text-emerald-100 shadow-[0_12px_30px_rgba(16,185,129,0.12)]">
|
||||
Marked {{ number_format((int) $freshlyClearedCount) }} new {{ (int) $freshlyClearedCount === 1 ? 'comment' : 'comments' }} as read when you opened this inbox.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="rounded-2xl border border-sky-400/20 bg-sky-500/10 p-5 shadow-[0_18px_45px_rgba(10,132,255,0.10)]">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-sky-200/75">Total comments</p>
|
||||
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($stats['total'] ?? 0)) }}</p>
|
||||
<p class="mt-2 text-sm text-sky-100/70">All comments on your approved artworks.</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-emerald-400/20 bg-emerald-500/10 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-emerald-200/75">Last 7 days</p>
|
||||
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($stats['recent'] ?? 0)) }}</p>
|
||||
<p class="mt-2 text-sm text-emerald-100/70">Recent conversation momentum.</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-violet-400/20 bg-violet-500/10 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-200/75">Unique commenters</p>
|
||||
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($stats['commenters'] ?? 0)) }}</p>
|
||||
<p class="mt-2 text-sm text-violet-100/70">Distinct people engaging with your work.</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-amber-400/20 bg-amber-500/10 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-amber-200/75">Active artworks</p>
|
||||
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($stats['artworks'] ?? 0)) }}</p>
|
||||
<p class="mt-2 text-sm text-amber-100/70">Artworks that received comments.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 rounded-[26px] border border-white/[0.07] bg-white/[0.03] p-4 shadow-[0_18px_45px_rgba(0,0,0,0.18)] md:p-5">
|
||||
<form method="GET" action="{{ route('dashboard.comments.received') }}" class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="flex flex-1 flex-col gap-3 sm:flex-row">
|
||||
<label class="relative flex-1">
|
||||
<span class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-white/30">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
</span>
|
||||
<input
|
||||
type="search"
|
||||
name="q"
|
||||
value="{{ $search }}"
|
||||
placeholder="Search comment text, artwork title, or username"
|
||||
class="w-full rounded-2xl border border-white/[0.08] bg-black/20 py-3 pl-11 pr-4 text-sm text-white placeholder:text-white/30 outline-none transition focus:border-sky-400/40 focus:bg-black/30"
|
||||
>
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="sort"
|
||||
class="rounded-2xl border border-white/[0.08] bg-black/20 px-4 py-3 text-sm text-white outline-none transition focus:border-sky-400/40 focus:bg-black/30"
|
||||
>
|
||||
<option value="newest" @selected($sort === 'newest')>Newest first</option>
|
||||
<option value="oldest" @selected($sort === 'oldest')>Oldest first</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-sky-500 px-4 py-3 text-sm font-semibold text-white transition hover:bg-sky-400"
|
||||
>
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
Apply
|
||||
</button>
|
||||
|
||||
@if($search !== '' || $sort !== 'newest')
|
||||
<a
|
||||
href="{{ route('dashboard.comments.received') }}"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-white/[0.08] bg-white/[0.03] px-4 py-3 text-sm font-medium text-white/65 transition hover:bg-white/[0.07] hover:text-white"
|
||||
>
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
Reset
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if($comments->isEmpty())
|
||||
<div class="rounded-[28px] border border-white/[0.07] bg-white/[0.025] px-8 py-16 text-center shadow-[0_18px_45px_rgba(0,0,0,0.18)]">
|
||||
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.03] text-white/35">
|
||||
<i class="fa-regular fa-comments text-2xl"></i>
|
||||
</div>
|
||||
<h2 class="mt-5 text-2xl font-semibold text-white">No comments found</h2>
|
||||
<p class="mx-auto mt-3 max-w-xl text-sm leading-6 text-white/45">
|
||||
@if($search !== '')
|
||||
Nothing matched your current search. Try a shorter phrase, a username, or reset the filters.
|
||||
@else
|
||||
When members comment on your artworks, they will appear here with artwork previews and quick context.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="grid gap-5 lg:grid-cols-2 2xl:grid-cols-3">
|
||||
@foreach($comments as $comment)
|
||||
@php
|
||||
$author = $comment->user;
|
||||
$artwork = $comment->artwork;
|
||||
$profileUrl = !empty($author?->username)
|
||||
? '/@' . $author->username
|
||||
: '/profile/' . (int) ($author?->id ?? 0);
|
||||
$artworkUrl = $artwork
|
||||
? '/art/' . (int) $artwork->id . '/' . \Illuminate\Support\Str::slug($artwork->title ?? $artwork->name ?? 'artwork')
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
<article class="group flex h-full flex-col overflow-hidden rounded-[28px] border border-white/[0.07] bg-white/[0.025] shadow-[0_18px_45px_rgba(0,0,0,0.18)] transition hover:border-sky-400/20 hover:bg-white/[0.035]">
|
||||
<div class="border-b border-white/[0.06] bg-black/20">
|
||||
@if($artwork)
|
||||
<a href="{{ $artworkUrl }}" class="block">
|
||||
<img
|
||||
src="{{ $artwork->thumb_url ?? $artwork->thumb }}"
|
||||
alt="{{ $artwork->title ?? $artwork->name ?? 'Artwork' }}"
|
||||
class="h-52 w-full object-cover transition duration-500 group-hover:scale-[1.02]"
|
||||
>
|
||||
</a>
|
||||
@else
|
||||
<div class="flex h-52 items-center justify-center text-white/25">
|
||||
<i class="fa-regular fa-image text-4xl"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 flex-col p-5 md:p-6">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<a href="{{ $profileUrl }}" class="shrink-0">
|
||||
<img
|
||||
src="{{ \App\Support\AvatarUrl::forUser((int) ($author?->id ?? 0), $author?->profile?->avatar_hash, 64) }}"
|
||||
alt="{{ $author?->name ?? $author?->username ?? 'Member' }}"
|
||||
class="h-12 w-12 rounded-full object-cover ring-1 ring-white/[0.10]"
|
||||
>
|
||||
</a>
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<a href="{{ $profileUrl }}" class="truncate text-base font-semibold text-white transition hover:text-sky-300">
|
||||
{{ $author?->name ?? $author?->username ?? 'Unknown member' }}
|
||||
</a>
|
||||
@if(!empty($author?->username))
|
||||
<span class="truncate text-xs font-medium uppercase tracking-[0.18em] text-white/30">{{ '@' . $author->username }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-white/45">
|
||||
{{ optional($comment->created_at)->diffForHumans() ?? 'Unknown time' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs font-medium">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-white/60">
|
||||
<i class="fa-solid fa-message"></i>
|
||||
Comment #{{ $comment->id }}
|
||||
</span>
|
||||
@if($artworkUrl)
|
||||
<a href="{{ $artworkUrl }}" 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 transition hover:bg-sky-500/20">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
Open artwork
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 rounded-2xl border border-white/[0.06] bg-black/20 p-4 md:p-5">
|
||||
<div class="prose prose-invert prose-sm max-w-none prose-p:text-white/80 prose-a:text-sky-300 prose-strong:text-white prose-code:text-sky-200">
|
||||
{!! $comment->getDisplayHtml() !!}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto pt-5">
|
||||
<div class="flex flex-col gap-3 border-t border-white/[0.06] pt-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-white/30">Artwork</p>
|
||||
@if($artwork)
|
||||
<a href="{{ $artworkUrl }}" class="mt-1 block truncate text-sm font-medium text-white/80 transition hover:text-sky-300">
|
||||
{{ $artwork->title ?? $artwork->name ?? 'Untitled artwork' }}
|
||||
</a>
|
||||
@else
|
||||
<p class="mt-1 text-sm text-white/35">This artwork is no longer available.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-white/45">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-white/[0.03] px-3 py-1">
|
||||
<i class="fa-regular fa-clock"></i>
|
||||
{{ optional($comment->created_at)->format('M j, Y H:i') ?? 'Unknown date' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-center">
|
||||
{{ $comments->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,76 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
$galleryItems = collect($artworks->items())->map(fn ($art) => [
|
||||
'id' => $art->id ?? null,
|
||||
'name' => $art->name ?? $art->title ?? null,
|
||||
'title' => $art->title ?? $art->name ?? null,
|
||||
'thumb' => $art->thumb ?? $art->thumb_url ?? null,
|
||||
'thumb_url' => $art->thumb_url ?? $art->thumb ?? null,
|
||||
'slug' => $art->slug ?? '',
|
||||
'author' => $art->author ?? '',
|
||||
'uname' => $art->uname ?? $art->author ?? '',
|
||||
'username' => $art->username ?? '',
|
||||
'avatar_url' => $art->avatar_url ?? null,
|
||||
'category_name' => $art->category_name ?? '',
|
||||
'category_slug' => $art->category_slug ?? '',
|
||||
'width' => $art->width ?? null,
|
||||
'height' => $art->height ?? null,
|
||||
'likes' => $art->likes ?? 0,
|
||||
'comments_count' => $art->comments_count ?? 0,
|
||||
])->values();
|
||||
@endphp
|
||||
|
||||
@push('scripts')
|
||||
@vite('resources/js/entry-masonry-gallery.jsx')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<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="My Favourites"
|
||||
icon="fa-heart"
|
||||
:breadcrumbs="collect([
|
||||
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
|
||||
(object) ['name' => 'Favourites', 'url' => route('dashboard.favorites')],
|
||||
])"
|
||||
description="Artworks you saved, displayed in the same gallery layout as Browse."
|
||||
actionsClass="lg:pt-8"
|
||||
>
|
||||
<x-slot name="actions">
|
||||
@if($artworks->total() > 0)
|
||||
<div class="inline-flex items-center gap-1.5 rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-1.5 text-sm text-white/60">
|
||||
<i class="fa-solid fa-images text-xs text-sky-300"></i>
|
||||
<span>{{ number_format($artworks->total()) }} artworks</span>
|
||||
</div>
|
||||
@endif
|
||||
</x-slot>
|
||||
</x-nova-page-header>
|
||||
|
||||
<section class="px-6 pb-10 pt-8 md:px-10">
|
||||
@if($artworks->isEmpty())
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-8 text-center text-white/60">
|
||||
You have no favourites yet.
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
data-react-masonry-gallery
|
||||
data-artworks='@json($galleryItems)'
|
||||
data-gallery-type="dashboard-favorites"
|
||||
@if($artworks->nextPageUrl()) data-next-page-url="{{ $artworks->nextPageUrl() }}" @endif
|
||||
data-limit="20"
|
||||
class="min-h-32"
|
||||
></div>
|
||||
@endif
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,288 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<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>
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,289 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<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 I Follow"
|
||||
icon="fa-user-group"
|
||||
:breadcrumbs="collect([
|
||||
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
|
||||
(object) ['name' => 'Following', 'url' => route('dashboard.following')],
|
||||
])"
|
||||
description="Creators and members you follow, with quick stats and recent follow time."
|
||||
actionsClass="lg:pt-8"
|
||||
>
|
||||
<x-slot name="actions">
|
||||
<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
|
||||
|
||||
<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>
|
||||
<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
|
||||
$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
|
||||
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
@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>
|
||||
</a>
|
||||
|
||||
<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="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" 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">
|
||||
{{ $following->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,32 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto py-8">
|
||||
<h1 class="text-2xl font-semibold mb-4">My Gallery</h1>
|
||||
|
||||
@if($artworks->isEmpty())
|
||||
<p class="text-sm text-gray-500">You have not uploaded any artworks yet.</p>
|
||||
@else
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
@foreach($artworks as $art)
|
||||
<div class="space-y-3">
|
||||
<x-artwork-card :art="$art" />
|
||||
<div class="rounded-xl border border-white/5 bg-white/[0.03] px-3 py-2">
|
||||
<div class="text-xs text-soft">Published: {{ optional($art->published_at)->format('Y-m-d') }}</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<a href="{{ route('dashboard.artworks.edit', ['id' => $art->id]) }}" class="text-xs px-2 py-1 bg-black/10 rounded">Edit</a>
|
||||
<form method="POST" action="{{ route('dashboard.artworks.destroy', ['id' => $art->id]) }}" onsubmit="return confirm('Really delete this artwork?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-xs px-2 py-1 bg-red-600 text-white rounded">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-6">{{ $artworks->links() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,187 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<x-nova-page-header
|
||||
section="Dashboard"
|
||||
title="Notifications"
|
||||
icon="fa-bell"
|
||||
:breadcrumbs="collect([
|
||||
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
|
||||
(object) ['name' => 'Notifications', 'url' => route('dashboard.notifications')],
|
||||
])"
|
||||
description="A dedicated feed for follows, likes, comments, and system events so you can triage account activity quickly."
|
||||
>
|
||||
<x-slot name="actions">
|
||||
<form id="mark-all-notifications-read-form" class="contents">
|
||||
<button
|
||||
type="button"
|
||||
id="mark-all-notifications-read"
|
||||
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-check-double text-xs"></i>
|
||||
Mark all read
|
||||
</button>
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-nova-page-header>
|
||||
|
||||
<div class="px-6 pb-16 pt-8 md:px-10">
|
||||
<div class="mb-6 flex flex-wrap items-center gap-2 rounded-2xl border border-white/[0.06] bg-white/[0.025] p-2 shadow-[0_12px_30px_rgba(0,0,0,0.14)]">
|
||||
<a href="{{ route('dashboard.comments.received') }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.05] hover:text-white">
|
||||
<i class="fa-solid fa-inbox"></i>
|
||||
Received
|
||||
</a>
|
||||
<a href="{{ route('messages.index') }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.05] hover:text-white">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
Messages
|
||||
</a>
|
||||
<a href="{{ route('dashboard.notifications') }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition {{ request()->routeIs('dashboard.notifications') ? 'bg-sky-500/15 text-sky-200 ring-1 ring-sky-400/20' : 'text-white/60 hover:bg-white/[0.05] hover:text-white' }}">
|
||||
<i class="fa-solid fa-bell"></i>
|
||||
Notifications
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<div class="rounded-2xl border border-sky-400/20 bg-sky-500/10 p-5 shadow-[0_18px_45px_rgba(10,132,255,0.10)]">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-sky-200/75">Unread</p>
|
||||
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($unreadCount) }}</p>
|
||||
<p class="mt-2 text-sm text-sky-100/70">Notifications that still need attention.</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-violet-400/20 bg-violet-500/10 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-200/75">Loaded</p>
|
||||
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($notifications->count()) }}</p>
|
||||
<p class="mt-2 text-sm text-violet-100/70">Items shown on this page.</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-amber-400/20 bg-amber-500/10 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-amber-200/75">Total feed</p>
|
||||
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($notificationsMeta['total'] ?? 0)) }}</p>
|
||||
<p class="mt-2 text-sm text-amber-100/70">All notifications stored for your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($notifications->isEmpty())
|
||||
<div class="rounded-[28px] border border-white/[0.07] bg-white/[0.025] px-8 py-16 text-center shadow-[0_18px_45px_rgba(0,0,0,0.18)]">
|
||||
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.03] text-white/35">
|
||||
<i class="fa-regular fa-bell text-2xl"></i>
|
||||
</div>
|
||||
<h2 class="mt-5 text-2xl font-semibold text-white">No notifications yet</h2>
|
||||
<p class="mx-auto mt-3 max-w-xl text-sm leading-6 text-white/45">
|
||||
Follows, comments, likes, and system notices will appear here as your account becomes more active.
|
||||
</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-4">
|
||||
@foreach($notifications as $item)
|
||||
@php
|
||||
$isUnread = !($item['read'] ?? false);
|
||||
$actor = $item['actor'] ?? null;
|
||||
$destination = $item['url'] ?: route('dashboard.comments.received');
|
||||
@endphp
|
||||
<a
|
||||
href="{{ $destination }}"
|
||||
class="block rounded-[24px] border p-5 transition hover:bg-white/[0.04] {{ $isUnread ? 'border-sky-400/20 bg-sky-500/[0.08]' : 'border-white/[0.07] bg-white/[0.025]' }}"
|
||||
data-notification-link
|
||||
data-notification-id="{{ $item['id'] }}"
|
||||
data-notification-read="{{ ($item['read'] ?? false) ? '1' : '0' }}"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<img
|
||||
src="{{ $actor['avatar_url'] ?? 'https://files.skinbase.org/default/avatar_default.webp' }}"
|
||||
alt="{{ $actor['name'] ?? 'Notification' }}"
|
||||
class="h-12 w-12 rounded-full object-cover ring-1 ring-white/10"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-white/90">{{ $item['message'] ?? 'New activity' }}</p>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs uppercase tracking-[0.16em] text-white/30">
|
||||
<span>{{ $item['type'] ?? 'notification' }}</span>
|
||||
<span>{{ $item['time_ago'] ?? '' }}</span>
|
||||
@if(!empty($actor['username']))
|
||||
<span>{{ '@' . $actor['username'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if($isUnread)
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-sky-400/25 bg-sky-500/12 px-3 py-1 text-xs font-semibold text-sky-200">
|
||||
<i class="fa-solid fa-circle text-[8px]"></i>
|
||||
Unread
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-xs font-semibold text-white/50">
|
||||
Read
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(($notificationsMeta['last_page'] ?? 1) > 1)
|
||||
<div class="mt-8 flex flex-wrap justify-center gap-2">
|
||||
@for($page = 1; $page <= (int) ($notificationsMeta['last_page'] ?? 1); $page++)
|
||||
<a
|
||||
href="{{ route('dashboard.notifications', ['page' => $page]) }}"
|
||||
class="inline-flex h-10 min-w-10 items-center justify-center rounded-xl border px-3 text-sm font-medium transition {{ (int) ($notificationsMeta['current_page'] ?? 1) === $page ? 'border-sky-400/30 bg-sky-500/15 text-sky-200' : 'border-white/[0.08] bg-white/[0.03] text-white/60 hover:bg-white/[0.06] hover:text-white' }}"
|
||||
>
|
||||
{{ $page }}
|
||||
</a>
|
||||
@endfor
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(function () {
|
||||
var csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
var markAllButton = document.getElementById('mark-all-notifications-read');
|
||||
|
||||
async function postJson(url) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
}
|
||||
|
||||
if (markAllButton) {
|
||||
markAllButton.addEventListener('click', async function () {
|
||||
markAllButton.disabled = true;
|
||||
try {
|
||||
await postJson('/api/notifications/read-all');
|
||||
window.location.reload();
|
||||
} catch (_error) {
|
||||
markAllButton.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-notification-link]').forEach(function (link) {
|
||||
link.addEventListener('click', async function (event) {
|
||||
if (link.dataset.notificationRead === '1') return;
|
||||
|
||||
event.preventDefault();
|
||||
try {
|
||||
await postJson('/api/notifications/' + link.dataset.notificationId + '/read');
|
||||
} catch (_error) {
|
||||
// Navigate even if mark-read fails.
|
||||
}
|
||||
window.location.assign(link.href);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
Reference in New Issue
Block a user