feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile

Forum:
- TipTap WYSIWYG editor with full toolbar
- @emoji-mart/react emoji picker (consistent with tweets)
- @mention autocomplete with user search API
- Fix PHP 8.4 parse errors in Blade templates
- Fix thread data display (paginator items)
- Align forum page widths to max-w-5xl

Discover:
- Extract shared _nav.blade.php partial
- Add missing nav links to for-you page
- Add Following link for authenticated users

Feed/Posts:
- Post model, controllers, policies, migrations
- Feed page components (PostComposer, FeedCard, etc)
- Post reactions, comments, saves, reports, sharing
- Scheduled publishing support
- Link preview controller

Profile:
- Profile page components (ProfileHero, ProfileTabs)
- Profile API controller

Uploads:
- Upload wizard enhancements
- Scheduled publish picker
- Studio status bar and readiness checklist
This commit is contained in:
2026-03-03 09:48:31 +01:00
parent 1266f81d35
commit dc51d65440
178 changed files with 14308 additions and 665 deletions

View File

@@ -1,49 +1,103 @@
@extends('layouts.nova')
@php($gridV2 = request()->query('grid') === 'v2')
@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
@push('head')
<style>
.nb-hero-fade {
background: linear-gradient(180deg, rgba(17,24,39,0) 0%, rgba(7,10,15,0.9) 60%, rgba(7,10,15,1) 100%);
}
.nb-hero-gradient {
background: linear-gradient(135deg, rgba(224,122,33,0.08) 0%, rgba(15,23,36,0) 50%, rgba(21,36,58,0.4) 100%);
animation: nb-hero-shimmer 8s ease-in-out infinite alternate;
}
@keyframes nb-hero-shimmer {
0% { opacity: 0.6; }
100% { opacity: 1; }
}
</style>
@endpush
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-4">Favourites</h1>
<div class="container-fluid legacy-page">
<div class="pt-0">
<div class="mx-auto w-full">
<div class="relative min-h-[calc(100vh-64px)]">
<main class="w-full">
<div class="relative overflow-hidden nb-hero-radial">
<div class="absolute inset-0 nb-hero-gradient" aria-hidden="true"></div>
<div class="absolute inset-0 opacity-20 bg-[radial-gradient(ellipse_80%_60%_at_50%_-10%,#E07A2130,transparent)]" aria-hidden="true"></div>
<div class="mb-4 flex items-center justify-between">
<div class="text-sm text-muted">Showing your favourites</div>
<div>
<form method="GET" class="inline">
<label class="text-sm mr-2">Sort</label>
<select name="sort" onchange="this.form.submit()" class="rounded bg-panel px-2 py-1 text-sm">
<option value="newest" {{ ($sort ?? 'newest') === 'newest' ? 'selected' : '' }}>Newest first</option>
<option value="oldest" {{ ($sort ?? '') === 'oldest' ? 'selected' : '' }}>Oldest first</option>
</select>
</form>
<div class="relative px-6 py-10 md:px-10 md:py-14">
<nav class="flex items-center gap-1.5 flex-wrap text-sm text-neutral-400" aria-label="Breadcrumb">
<a class="hover:text-white transition-colors" href="/browse">Gallery</a>
<span class="opacity-40" aria-hidden="true"></span>
<span class="text-white/85">Favourites</span>
</nav>
<div class="mt-4 py-5">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-white/95 leading-tight">
My Favourites
</h1>
<p class="mt-2 text-sm leading-6 text-neutral-400 max-w-xl">
Artworks you saved, displayed in the same gallery layout as Browse.
</p>
@if($artworks->total() > 0)
<div class="mt-3 flex items-center gap-1.5 text-xs text-neutral-500">
<svg class="h-3.5 w-3.5 text-accent/70" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>{{ number_format($artworks->total()) }} artworks</span>
</div>
@endif
</div>
</div>
<div class="absolute left-0 right-0 bottom-0 h-16 nb-hero-fade pointer-events-none" aria-hidden="true"></div>
</div>
<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>
@if($artworks->isEmpty())
<p class="text-sm text-gray-500">You have no favourites yet.</p>
@else
<section data-nova-gallery data-gallery-type="dashboard-favorites">
<div class="{{ $gridV2 ? 'gallery' : 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6' }}" data-gallery-grid>
@foreach($artworks as $art)
<div class="relative gallery-item">
<x-artwork-card :art="$art" />
<div class="absolute right-2 top-2 z-40">
<form method="POST" action="{{ route('dashboard.favorites.destroy', ['artwork' => $art->id]) }}" onsubmit="return confirm('Really remove from favourites?');">
@csrf
@method('DELETE')
<button type="submit" class="rounded-md border border-white/15 bg-black/60 px-2 py-1 text-xs text-red-300 hover:text-red-200">Remove</button>
</form>
</div>
</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