From 7da0fd39f718da0c6e4934fa6575650793df774b Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Tue, 17 Mar 2026 18:34:26 +0100 Subject: [PATCH] updated gallery --- TODO.md | 8 + app/Chat.php | 3 +- .../Legacy/TodayDownloadsController.php | 2 +- app/Http/Controllers/News/NewsController.php | 37 ++-- app/Http/Controllers/StoryController.php | 3 + .../Controllers/User/TopAuthorsController.php | 4 +- app/Services/ArtworkService.php | 2 +- app/Services/TagService.php | 17 ++ app/Support/AvatarUrl.php | 3 +- docs/legacy-routes-inventory.md | 2 +- docs/registration-antispam.md | 27 ++- resources/js/Pages/Forum/ForumEditPost.jsx | 33 +++- resources/js/Pages/Forum/ForumNewThread.jsx | 30 ++- resources/js/Pages/Forum/ForumThread.jsx | 2 + resources/js/Pages/Home/HomeFresh.jsx | 61 +----- resources/js/Pages/Home/HomeTrending.jsx | 61 +----- .../js/Pages/Home/HomeTrendingForYou.jsx | 48 +---- resources/js/components/Topbar.jsx | 1 + resources/js/components/forum/ReplyForm.jsx | 30 ++- .../gallery/CategoryPillCarousel.css | 20 -- .../gallery/CategoryPillCarousel.jsx | 53 +++-- .../js/components/gallery/MasonryGallery.jsx | 53 +++-- .../js/components/profile/ProfileHero.jsx | 40 +++- .../components/profile/tabs/TabFavourites.jsx | 20 +- .../js/components/ui/NovaConfirmDialog.jsx | 94 +++++++++ resources/views/_legacy/_toolbar.blade.php | 5 +- .../views/_legacy/latest-comments.blade.php | 23 ++- .../_legacy/monthly-commentators.blade.php | 23 ++- resources/views/_legacy/profile.blade.php | 1 - resources/views/dashboard/comments.blade.php | 29 ++- resources/views/dashboard/favorites.blade.php | 67 ++----- resources/views/dashboard/following.blade.php | 181 ++++++++++-------- .../layouts/nova/content-layout.blade.php | 48 +++-- .../views/layouts/nova/toolbar.blade.php | 12 +- resources/views/news/_article_card.blade.php | 54 +++--- resources/views/news/_sidebar.blade.php | 75 ++++---- resources/views/news/category.blade.php | 54 ++++-- resources/views/news/index.blade.php | 128 ++++++++----- resources/views/news/layout.blade.php | 17 +- resources/views/news/show.blade.php | 138 +++++++------ resources/views/news/tag.blade.php | 53 +++-- resources/views/web/authors/top.blade.php | 45 +++-- resources/views/web/category.blade.php | 1 - .../views/web/comments/monthly.blade.php | 30 +-- resources/views/web/creators/rising.blade.php | 39 ++-- resources/views/web/daily-uploads.blade.php | 35 ++-- .../views/web/discover/for-you.blade.php | 55 +++--- resources/views/web/discover/index.blade.php | 54 +++--- .../views/web/featured-artworks.blade.php | 119 ++++++++---- .../views/web/stories/dashboard.blade.php | 80 ++++++-- resources/views/web/stories/index.blade.php | 32 ++-- routes/legacy.php | 34 ++-- 52 files changed, 1216 insertions(+), 870 deletions(-) create mode 100644 TODO.md create mode 100644 resources/js/components/ui/NovaConfirmDialog.jsx diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..a10791af --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# TODO SKINBASE NOVA + +## FORUM + +- [ ] we need to add in a main search (toolbar) and a search in the forum (search bar in the forum page) + +## ARTWORKS +- [ ] http://skinbase26.test/art/69601/testna-slika => we shouldnt display follow for yourself \ No newline at end of file diff --git a/app/Chat.php b/app/Chat.php index f23720c5..71f734de 100644 --- a/app/Chat.php +++ b/app/Chat.php @@ -81,7 +81,8 @@ class Chat echo '
'; if (!empty($_SESSION['web_login']['status'])) { - echo '
'; + echo ''; + echo csrf_field(); echo '
'; echo ''; echo '
'; diff --git a/app/Http/Controllers/Legacy/TodayDownloadsController.php b/app/Http/Controllers/Legacy/TodayDownloadsController.php index a5da43b8..1743fb85 100644 --- a/app/Http/Controllers/Legacy/TodayDownloadsController.php +++ b/app/Http/Controllers/Legacy/TodayDownloadsController.php @@ -63,6 +63,6 @@ class TodayDownloadsController extends Controller $page_title = 'Today Downloaded Artworks'; - return view('legacy::browse', ['page_title' => $page_title, 'artworks' => $paginator]); + return view('web.downloads.today', ['page_title' => $page_title, 'artworks' => $paginator]); } } diff --git a/app/Http/Controllers/News/NewsController.php b/app/Http/Controllers/News/NewsController.php index d7d145b1..fa00917f 100644 --- a/app/Http/Controllers/News/NewsController.php +++ b/app/Http/Controllers/News/NewsController.php @@ -35,21 +35,10 @@ class NewsController extends Controller } $articles = $query->paginate($perPage); - $categories = NewsCategory::active()->withCount('publishedArticles')->ordered()->get(); - $trending = NewsArticle::published() - ->orderByDesc('views') - ->limit(config('news.trending_limit', 5)) - ->get(['id', 'title', 'slug', 'views', 'published_at']); - - $tags = NewsTag::has('articles')->orderBy('name')->get(); - return view('news.index', [ 'featured' => $featured, 'articles' => $articles, - 'categories' => $categories, - 'trending' => $trending, - 'tags' => $tags, - ]); + ] + $this->sidebarData()); } // ----------------------------------------------------------------------- @@ -67,13 +56,10 @@ class NewsController extends Controller ->orderByDesc('published_at') ->paginate($perPage); - $categories = NewsCategory::active()->withCount('publishedArticles')->ordered()->get(); - return view('news.category', [ 'category' => $category, 'articles' => $articles, - 'categories' => $categories, - ]); + ] + $this->sidebarData()); } // ----------------------------------------------------------------------- @@ -91,13 +77,10 @@ class NewsController extends Controller ->orderByDesc('published_at') ->paginate($perPage); - $categories = NewsCategory::active()->withCount('publishedArticles')->ordered()->get(); - return view('news.tag', [ 'tag' => $tag, 'articles' => $articles, - 'categories' => $categories, - ]); + ] + $this->sidebarData()); } // ----------------------------------------------------------------------- @@ -126,7 +109,7 @@ class NewsController extends Controller return view('news.show', [ 'article' => $article, 'related' => $related, - ]); + ] + $this->sidebarData()); } // ----------------------------------------------------------------------- @@ -154,4 +137,16 @@ class NewsController extends Controller $request->session()->put($session, true); } + + private function sidebarData(): array + { + return [ + 'categories' => NewsCategory::active()->withCount('publishedArticles')->ordered()->get(), + 'trending' => NewsArticle::published() + ->orderByDesc('views') + ->limit(config('news.trending_limit', 5)) + ->get(['id', 'title', 'slug', 'views', 'published_at']), + 'tags' => NewsTag::has('articles')->orderBy('name')->get(), + ]; + } } diff --git a/app/Http/Controllers/StoryController.php b/app/Http/Controllers/StoryController.php index 51dcf560..db1494e1 100644 --- a/app/Http/Controllers/StoryController.php +++ b/app/Http/Controllers/StoryController.php @@ -79,6 +79,9 @@ class StoryController extends Controller 'page_meta_description' => 'Long-form creator stories, tutorials, interviews and project breakdowns on Skinbase.', 'page_canonical' => route('stories.index'), 'page_robots' => 'index,follow', + 'breadcrumbs' => collect([ + (object) ['name' => 'Stories', 'url' => route('stories.index')], + ]), ]); } diff --git a/app/Http/Controllers/User/TopAuthorsController.php b/app/Http/Controllers/User/TopAuthorsController.php index 1c62cb2c..7833aaaa 100644 --- a/app/Http/Controllers/User/TopAuthorsController.php +++ b/app/Http/Controllers/User/TopAuthorsController.php @@ -6,8 +6,6 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use App\Models\Artwork; -use App\Models\ArtworkStats; -use App\Models\User; class TopAuthorsController extends Controller { @@ -51,7 +49,7 @@ class TopAuthorsController extends Controller ]; }); - $page_title = 'Top Authors'; + $page_title = 'Top Creators'; return view('web.authors.top', compact('page_title', 'authors', 'metric')); } diff --git a/app/Services/ArtworkService.php b/app/Services/ArtworkService.php index d9757a28..822c5823 100644 --- a/app/Services/ArtworkService.php +++ b/app/Services/ArtworkService.php @@ -276,7 +276,7 @@ class ArtworkService $q->where('af.type', $type); }) ->with([ - 'user:id,name', + 'user:id,name,username', 'categories' => function ($q) { $q->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order'); }, diff --git a/app/Services/TagService.php b/app/Services/TagService.php index 405a3e4c..9c8e5a7f 100644 --- a/app/Services/TagService.php +++ b/app/Services/TagService.php @@ -254,8 +254,25 @@ final class TagService public function updateUsageCount(Tag $tag): void { + $this->syncUsageCount($tag); + } + + /** + * Recalculate and persist the real usage_count from artwork_tag. + * + * @return array{before:int, after:int, changed:bool} + */ + public function syncUsageCount(Tag $tag): array + { + $before = (int) $tag->usage_count; $count = (int) DB::table('artwork_tag')->where('tag_id', $tag->id)->count(); $tag->forceFill(['usage_count' => $count])->save(); + + return [ + 'before' => $before, + 'after' => $count, + 'changed' => $before !== $count, + ]; } /** diff --git a/app/Support/AvatarUrl.php b/app/Support/AvatarUrl.php index d7930c08..6b1415ae 100644 --- a/app/Support/AvatarUrl.php +++ b/app/Support/AvatarUrl.php @@ -30,7 +30,8 @@ class AvatarUrl $diskPath = sprintf('avatars/%s/%s/%s/%d.webp', $p1, $p2, $avatarHash, $size); // Always use CDN-hosted avatar files. - return sprintf('%s/avatars/%s/%s/%s/%d.webp?v=%s', $base, $p1, $p2, $avatarHash, $size, $avatarHash); + //return sprintf('%s/avatars/%s/%s/%s/%d.webp?v=%s', $base, $p1, $p2, $avatarHash, $size, $avatarHash); + return sprintf('%s/avatars/%s/%s/%s/%d.webp', $base, $p1, $p2, $avatarHash, $size); } public static function default(): string diff --git a/docs/legacy-routes-inventory.md b/docs/legacy-routes-inventory.md index 9081b728..e91c331c 100644 --- a/docs/legacy-routes-inventory.md +++ b/docs/legacy-routes-inventory.md @@ -33,7 +33,7 @@ Companion execution guide: [docs/legacy-routes-removal-checklist.md](docs/legacy | Method | Path | Route Name | Handler / Target | |---|---|---|---| -| GET | /chat | legacy.chat | ChatController@index | +| GET | /chat | legacy.chat | 301 -> /community/chat | | POST | /chat_post | legacy.chat.post | ChatController@post | | GET | /uploads/latest | uploads.latest | LatestController@index | | GET | /uploads/daily | uploads.daily | DailyUploadsController@index | diff --git a/docs/registration-antispam.md b/docs/registration-antispam.md index 99cd31ea..fc491e03 100644 --- a/docs/registration-antispam.md +++ b/docs/registration-antispam.md @@ -47,20 +47,23 @@ On repeated requests within cooldown: - No additional verification email is queued - Generic success message is returned -### 3) Progressive CAPTCHA (Turnstile) +### 3) Progressive CAPTCHA Service: -- `app/Services/Security/TurnstileVerifier.php` +- `app/Services/Security/CaptchaVerifier.php` +- `app/Services/Security/TurnstileVerifier.php` (legacy compatibility wrapper) -Controller logic (`RegisteredUserController::shouldRequireTurnstile`): +Controller logic (`RegisteredUserController::shouldRequireCaptcha`): -- Requires Turnstile for suspicious IP activity (attempt threshold) -- Also requires Turnstile when registration rate-limit state is detected +- Requires CAPTCHA for suspicious IP activity (attempt threshold) +- Also requires CAPTCHA when registration rate-limit state is detected +- Active provider is selected through `forum_bot_protection.captcha.provider` UI behavior (`resources/views/auth/register.blade.php`): -- Turnstile widget is only rendered when required +- Provider-specific widget is only rendered when required +- Turnstile, reCAPTCHA, and hCaptcha are supported ### 4) Disposable Domain Block @@ -153,9 +156,10 @@ Key settings: - `monthly_email_limit` - `generic_success_message` -Turnstile config: +Captcha provider config: -- `config/services.php` under `turnstile` +- `config/services.php` under `turnstile`, `recaptcha`, and `hcaptcha` +- `config/forum_bot_protection.php` under `captcha` Environment examples: @@ -189,7 +193,7 @@ Covered scenarios: - Cooldown suppresses extra sends - Disposable domains blocked - Quota exceeded blocks send and keeps generic success UX -- Turnstile required on abuse/rate-limit state +- CAPTCHA required on abuse/rate-limit state - Tokens hashed, expire, and are one-time - Responses avoid account enumeration @@ -199,4 +203,7 @@ Covered scenarios: - Ensure queue workers process the `mail` queue. - Monitor `email_send_events` for blocked/sent patterns. - Set `REGISTRATION_MONTHLY_EMAIL_LIMIT` based on provider quota. -- Configure `TURNSTILE_SITE_KEY` and `TURNSTILE_SECRET_KEY` in production. +- Configure the active CAPTCHA provider keys in production: + - Turnstile: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY` + - reCAPTCHA: `RECAPTCHA_ENABLED`, `RECAPTCHA_SITE_KEY`, `RECAPTCHA_SECRET_KEY` + - hCaptcha: `HCAPTCHA_ENABLED`, `HCAPTCHA_SITE_KEY`, `HCAPTCHA_SECRET_KEY` diff --git a/resources/js/Pages/Forum/ForumEditPost.jsx b/resources/js/Pages/Forum/ForumEditPost.jsx index 0a25d40d..8eb91dcd 100644 --- a/resources/js/Pages/Forum/ForumEditPost.jsx +++ b/resources/js/Pages/Forum/ForumEditPost.jsx @@ -2,9 +2,12 @@ import React, { useState, useCallback } from 'react' import Breadcrumbs from '../../components/forum/Breadcrumbs' import Button from '../../components/ui/Button' import RichTextEditor from '../../components/forum/RichTextEditor' +import TurnstileField from '../../components/security/TurnstileField' +import { populateBotFingerprint } from '../../lib/security/botFingerprint' -export default function ForumEditPost({ post, thread, csrfToken, errors = {} }) { +export default function ForumEditPost({ post, thread, csrfToken, errors = {}, captcha = {} }) { const [content, setContent] = useState(post?.content ?? '') + const [captchaToken, setCaptchaToken] = useState('') const [submitting, setSubmitting] = useState(false) const breadcrumbs = [ @@ -18,6 +21,10 @@ export default function ForumEditPost({ post, thread, csrfToken, errors = {} }) if (submitting) return setSubmitting(true) // Let the form submit normally for PRG + populateBotFingerprint(e.currentTarget).finally(() => { + e.currentTarget.submit() + }) + e.preventDefault() }, [submitting]) return ( @@ -39,6 +46,20 @@ export default function ForumEditPost({ post, thread, csrfToken, errors = {} }) > + + + + + {errors.bot ? ( +
+ {Array.isArray(errors.bot) ? errors.bot[0] : errors.bot} +
+ ) : null} + {errors.captcha ? ( +
+ {Array.isArray(errors.captcha) ? errors.captcha[0] : errors.captcha} +
+ ) : null} {/* Rich text editor */}
@@ -56,6 +77,16 @@ export default function ForumEditPost({ post, thread, csrfToken, errors = {} })
+ {captcha.siteKey ? ( + + ) : null} + {/* Actions */}
+ + + + + {errors.bot ? ( +
+ {Array.isArray(errors.bot) ? errors.bot[0] : errors.bot} +
+ ) : null} + {errors.captcha ? ( +
+ {Array.isArray(errors.captcha) ? errors.captcha[0] : errors.captcha} +
+ ) : null}
+ {captcha.siteKey ? ( + + ) : null} + {/* Submit */}
diff --git a/resources/js/Pages/Forum/ForumThread.jsx b/resources/js/Pages/Forum/ForumThread.jsx index e034228b..df038b98 100644 --- a/resources/js/Pages/Forum/ForumThread.jsx +++ b/resources/js/Pages/Forum/ForumThread.jsx @@ -20,6 +20,7 @@ export default function ForumThread({ canModerate = false, csrfToken = '', status = null, + captcha = {}, }) { const [currentSort, setCurrentSort] = useState(sort) @@ -161,6 +162,7 @@ export default function ForumThread({ prefill={replyPrefill} quotedAuthor={quotedPost?.user?.name} csrfToken={csrfToken} + captcha={captcha} /> ) ) : ( diff --git a/resources/js/Pages/Home/HomeFresh.jsx b/resources/js/Pages/Home/HomeFresh.jsx index e0a6d0f6..8375503a 100644 --- a/resources/js/Pages/Home/HomeFresh.jsx +++ b/resources/js/Pages/Home/HomeFresh.jsx @@ -1,55 +1,5 @@ import React from 'react' - -const FALLBACK = 'https://files.skinbase.org/default/missing_md.webp' -const AVATAR_FALLBACK = 'https://files.skinbase.org/default/avatar_default.webp' - -function FreshCard({ item }) { - const username = item.author_username ? `@${item.author_username}` : null - - return ( - - ) -} +import ArtworkGalleryGrid from '../../components/artwork/ArtworkGalleryGrid' export default function HomeFresh({ items }) { if (!Array.isArray(items) || items.length === 0) return null @@ -63,11 +13,10 @@ export default function HomeFresh({ items }) {
-
- {items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => ( - - ))} -
+ ) } diff --git a/resources/js/Pages/Home/HomeTrending.jsx b/resources/js/Pages/Home/HomeTrending.jsx index 3f1d7aa9..8ff26c06 100644 --- a/resources/js/Pages/Home/HomeTrending.jsx +++ b/resources/js/Pages/Home/HomeTrending.jsx @@ -1,55 +1,5 @@ import React from 'react' - -const FALLBACK = 'https://files.skinbase.org/default/missing_md.webp' -const AVATAR_FALLBACK = 'https://files.skinbase.org/default/avatar_default.webp' - -function ArtCard({ item }) { - const username = item.author_username ? `@${item.author_username}` : null - - return ( - - ) -} +import ArtworkGalleryGrid from '../../components/artwork/ArtworkGalleryGrid' export default function HomeTrending({ items }) { if (!Array.isArray(items) || items.length === 0) return null @@ -65,11 +15,10 @@ export default function HomeTrending({ items }) {
-
- {items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => ( - - ))} -
+ ) } diff --git a/resources/js/Pages/Home/HomeTrendingForYou.jsx b/resources/js/Pages/Home/HomeTrendingForYou.jsx index 660e1b9b..99d04ed0 100644 --- a/resources/js/Pages/Home/HomeTrendingForYou.jsx +++ b/resources/js/Pages/Home/HomeTrendingForYou.jsx @@ -1,45 +1,5 @@ import React from 'react' - -const FALLBACK = 'https://files.skinbase.org/default/missing_md.webp' -const AVATAR_FALLBACK = 'https://files.skinbase.org/default/avatar_default.webp' - -function ArtCard({ item }) { - const username = item.author_username ? `@${item.author_username}` : null - return ( - - ) -} +import ArtworkGalleryGrid from '../../components/artwork/ArtworkGalleryGrid' /** * Personalized trending: artworks matching user's top tags, sorted by trending score. @@ -60,11 +20,7 @@ export default function HomeTrendingForYou({ items, preferences }) { See all → -
- {items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => ( - - ))} -
+ ) } diff --git a/resources/js/components/Topbar.jsx b/resources/js/components/Topbar.jsx index b1316cc7..846b9545 100644 --- a/resources/js/components/Topbar.jsx +++ b/resources/js/components/Topbar.jsx @@ -21,6 +21,7 @@ export default function Topbar({ user = null }) {
+ Community Forum {user ? ( diff --git a/resources/js/components/forum/ReplyForm.jsx b/resources/js/components/forum/ReplyForm.jsx index ce6a606e..325ee49e 100644 --- a/resources/js/components/forum/ReplyForm.jsx +++ b/resources/js/components/forum/ReplyForm.jsx @@ -1,9 +1,12 @@ import React, { useState, useRef, useCallback } from 'react' import Button from '../ui/Button' import RichTextEditor from './RichTextEditor' +import TurnstileField from '../security/TurnstileField' +import { buildBotFingerprint } from '../../lib/security/botFingerprint' -export default function ReplyForm({ topicKey, prefill = '', quotedAuthor = null, csrfToken }) { +export default function ReplyForm({ topicKey, prefill = '', quotedAuthor = null, csrfToken, captcha = {} }) { const [content, setContent] = useState(prefill) + const [captchaToken, setCaptchaToken] = useState('') const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const formRef = useRef(null) @@ -16,16 +19,24 @@ export default function ReplyForm({ topicKey, prefill = '', quotedAuthor = null, setError(null) try { + const fingerprint = await buildBotFingerprint() const res = await fetch(`/forum/topic/${topicKey}/reply`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken, + 'X-Bot-Fingerprint': fingerprint, + 'X-Captcha-Token': captchaToken, 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'same-origin', - body: JSON.stringify({ content: content.trim() }), + body: JSON.stringify({ + content: content.trim(), + homepage_url: '', + _bot_fingerprint: fingerprint, + [captcha.inputName || 'cf-turnstile-response']: captchaToken, + }), }) if (res.ok) { @@ -33,9 +44,10 @@ export default function ReplyForm({ topicKey, prefill = '', quotedAuthor = null, window.location.reload() } else if (res.status === 422) { const json = await res.json() - setError(json.errors?.content?.[0] ?? 'Validation error.') + setError(json.errors?.content?.[0] ?? json.errors?.bot?.[0] ?? json.message ?? 'Validation error.') } else { - setError('Failed to post reply. Please try again.') + const json = await res.json().catch(() => ({})) + setError(json?.errors?.bot?.[0] ?? json?.message ?? 'Failed to post reply. Please try again.') } } catch { setError('Network error. Please try again.') @@ -66,6 +78,16 @@ export default function ReplyForm({ topicKey, prefill = '', quotedAuthor = null, /> {/* Submit */} + {captcha.siteKey ? ( + + ) : null} +
- - ); } diff --git a/resources/js/components/gallery/MasonryGallery.jsx b/resources/js/components/gallery/MasonryGallery.jsx index 0acdb12e..023645b7 100644 --- a/resources/js/components/gallery/MasonryGallery.jsx +++ b/resources/js/components/gallery/MasonryGallery.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useCallback, memo, } from 'react'; -import ArtworkCard from './ArtworkCard'; +import ArtworkGallery from '../artwork/ArtworkGallery'; import './MasonryGallery.css'; // ── Masonry helpers ──────────────────────────────────────────────────────── @@ -132,6 +132,8 @@ function mapRankApiArtwork(item) { uname: item.author?.name ?? '', username: item.author?.username ?? item.author?.name ?? '', avatar_url: item.author?.avatar_url ?? null, + content_type_name: item.category?.content_type_name ?? item.category?.content_type_slug ?? item.category?.content_type ?? '', + content_type_slug: item.category?.content_type_slug ?? item.category?.content_type ?? '', category_name: item.category?.name ?? '', category_slug: item.category?.slug ?? '', slug: item.slug ?? '', @@ -164,6 +166,36 @@ async function fetchRankApiArtworks(endpoint, rankType) { const SKELETON_COUNT = 10; +function getMasonryCardProps(art, idx) { + const title = (art.name || art.title || 'Untitled artwork').trim(); + const hasDimensions = Number(art.width) > 0 && Number(art.height) > 0; + const aspectRatio = hasDimensions ? Number(art.width) / Number(art.height) : null; + const categorySlug = (art.category_slug || '').toLowerCase(); + const categoryName = (art.category_name || art.category || '').toLowerCase(); + const wideCategories = ['photography', 'wallpapers', 'photography-digital', 'wallpaper']; + const wideCategoryNames = ['photography', 'wallpapers']; + const isWideEligible = + aspectRatio !== null && + aspectRatio > 2.0 && + (wideCategories.includes(categorySlug) || wideCategoryNames.includes(categoryName)); + + return { + articleClassName: `nova-card gallery-item artwork relative${isWideEligible ? ' nova-card--wide' : ''}`, + articleStyle: isWideEligible ? { gridColumn: 'span 2' } : undefined, + frameClassName: 'rounded-2xl ring-1 ring-white/5 bg-black/20 shadow-lg shadow-black/40 hover:ring-white/15 hover:shadow-[0_8px_30px_rgba(0,0,0,0.6),0_0_0_1px_rgba(255,255,255,0.08)]', + mediaClassName: 'nova-card-media relative w-full overflow-hidden bg-neutral-900', + mediaStyle: hasDimensions ? { aspectRatio: `${art.width} / ${art.height}` } : undefined, + imageSrcSet: art.thumb_srcset || undefined, + imageSizes: '(max-width: 768px) 50vw, (max-width: 1280px) 33vw, 20vw', + imageWidth: hasDimensions ? art.width : undefined, + imageHeight: hasDimensions ? art.height : undefined, + loading: idx < 8 ? 'eager' : 'lazy', + decoding: idx < 8 ? 'sync' : 'async', + fetchPriority: idx === 0 ? 'high' : undefined, + imageClassName: 'nova-card-main-image absolute inset-0 h-full w-full object-cover group-hover:scale-[1.03]', + }; +} + // ── Main component ──────────────────────────────────────────────────────── /** * MasonryGallery @@ -309,7 +341,7 @@ function MasonryGallery({ // ── Render ───────────────────────────────────────────────────────────── return (
- {artworks.map((art, idx) => ( - - ))} +
{/* Infinite scroll sentinel – placed after the grid */} diff --git a/resources/js/components/profile/ProfileHero.jsx b/resources/js/components/profile/ProfileHero.jsx index 2ca0ac0b..09865721 100644 --- a/resources/js/components/profile/ProfileHero.jsx +++ b/resources/js/components/profile/ProfileHero.jsx @@ -1,4 +1,5 @@ import React, { useState } from 'react' +import NovaConfirmDialog from '../ui/NovaConfirmDialog' import ProfileCoverEditor from './ProfileCoverEditor' /** @@ -13,6 +14,8 @@ export default function ProfileHero({ user, profile, isOwner, viewerIsFollowing, const [editorOpen, setEditorOpen] = useState(false) const [coverUrl, setCoverUrl] = useState(user?.cover_url || heroBgUrl || null) const [coverPosition, setCoverPosition] = useState(Number.isFinite(user?.cover_position) ? user.cover_position : 50) + const [confirmOpen, setConfirmOpen] = useState(false) + const [pendingFollowState, setPendingFollowState] = useState(null) const uname = user.username || user.name || 'Unknown' const displayName = user.name || uname @@ -23,7 +26,7 @@ export default function ProfileHero({ user, profile, isOwner, viewerIsFollowing, const bio = profile?.bio || profile?.about || '' - const toggleFollow = async () => { + const persistFollowState = async (nextState) => { if (loading) return setLoading(true) try { @@ -43,6 +46,29 @@ export default function ProfileHero({ user, profile, isOwner, viewerIsFollowing, setLoading(false) } + const toggleFollow = async () => { + const nextState = !following + if (!nextState) { + setPendingFollowState(nextState) + setConfirmOpen(true) + return + } + + await persistFollowState(nextState) + } + + const onConfirmUnfollow = async () => { + if (pendingFollowState === null) return + setConfirmOpen(false) + await persistFollowState(pendingFollowState) + setPendingFollowState(null) + } + + const onCloseConfirm = () => { + setConfirmOpen(false) + setPendingFollowState(null) + } + return ( <>
@@ -228,6 +254,18 @@ export default function ProfileHero({ user, profile, isOwner, viewerIsFollowing, setCoverPosition(50) }} /> + + ) } diff --git a/resources/js/components/profile/tabs/TabFavourites.jsx b/resources/js/components/profile/tabs/TabFavourites.jsx index 14bffc29..3a6f7e69 100644 --- a/resources/js/components/profile/tabs/TabFavourites.jsx +++ b/resources/js/components/profile/tabs/TabFavourites.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import ArtworkCard from '../../gallery/ArtworkCard' +import ArtworkGallery from '../../artwork/ArtworkGallery' function FavSkeleton() { return ( @@ -57,16 +57,16 @@ export default function TabFavourites({ favourites, isOwner, username }) {
) : ( <> -
- {items.map((art, i) => ( - - ))} + ({ + loading: index < 8 ? 'eager' : 'lazy', + })} + > {loadingMore && Array.from({ length: 4 }).map((_, i) => )} -
+ {nextCursor && (
diff --git a/resources/js/components/ui/NovaConfirmDialog.jsx b/resources/js/components/ui/NovaConfirmDialog.jsx new file mode 100644 index 00000000..bd47ef60 --- /dev/null +++ b/resources/js/components/ui/NovaConfirmDialog.jsx @@ -0,0 +1,94 @@ +import React, { useEffect, useRef } from 'react' +import { createPortal } from 'react-dom' + +export default function NovaConfirmDialog({ + open, + title = 'Please confirm', + message, + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', + confirmTone = 'danger', + onConfirm, + onClose, + busy = false, +}) { + const backdropRef = useRef(null) + const cancelButtonRef = useRef(null) + + useEffect(() => { + if (!open) return undefined + const timeoutId = window.setTimeout(() => cancelButtonRef.current?.focus(), 60) + return () => window.clearTimeout(timeoutId) + }, [open]) + + useEffect(() => { + if (!open) return undefined + + const handleKeyDown = (event) => { + if (event.key === 'Escape' && !busy) { + onClose?.() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [busy, onClose, open]) + + if (!open) return null + + const confirmClassName = confirmTone === 'danger' + ? 'border border-rose-400/25 bg-rose-500/12 text-rose-100 hover:bg-rose-500/18 focus-visible:ring-rose-300/50' + : 'border border-accent/25 bg-accent/90 text-deep hover:brightness-110 focus-visible:ring-accent/50' + + return createPortal( +
{ + if (event.target === backdropRef.current && !busy) { + onClose?.() + } + }} + role="presentation" + > +
+
+

Skinbase Nova

+

+ {title} +

+
+ +
+

{message}

+
+ +
+ + +
+
+
, + document.body, + ) +} diff --git a/resources/views/_legacy/_toolbar.blade.php b/resources/views/_legacy/_toolbar.blade.php index 6659022a..158e0ce8 100644 --- a/resources/views/_legacy/_toolbar.blade.php +++ b/resources/views/_legacy/_toolbar.blade.php @@ -44,7 +44,8 @@
diff --git a/resources/views/_legacy/latest-comments.blade.php b/resources/views/_legacy/latest-comments.blade.php index 4918b067..9f11cc12 100644 --- a/resources/views/_legacy/latest-comments.blade.php +++ b/resources/views/_legacy/latest-comments.blade.php @@ -1,13 +1,22 @@ @extends('layouts.nova') +@php + $headerBreadcrumbs = collect([ + (object) ['name' => $page_title, 'url' => route('legacy.latest_comments')], + ]); +@endphp + @section('content') -
- + + +
@foreach ($comments as $comment) diff --git a/resources/views/_legacy/monthly-commentators.blade.php b/resources/views/_legacy/monthly-commentators.blade.php index 975e7175..4e2c2310 100644 --- a/resources/views/_legacy/monthly-commentators.blade.php +++ b/resources/views/_legacy/monthly-commentators.blade.php @@ -1,13 +1,22 @@ @extends('layouts.nova') +@php + $headerBreadcrumbs = collect([ + (object) ['name' => $page_title, 'url' => route('legacy.monthly_commentators')], + ]); +@endphp + @section('content') -
- + + +
diff --git a/resources/views/_legacy/profile.blade.php b/resources/views/_legacy/profile.blade.php index 8b6abd1b..3771db71 100644 --- a/resources/views/_legacy/profile.blade.php +++ b/resources/views/_legacy/profile.blade.php @@ -690,5 +690,4 @@ @endsection @push('scripts') - @endpush diff --git a/resources/views/dashboard/comments.blade.php b/resources/views/dashboard/comments.blade.php index 2579f129..cfbba4ee 100644 --- a/resources/views/dashboard/comments.blade.php +++ b/resources/views/dashboard/comments.blade.php @@ -1,17 +1,30 @@ @extends('layouts.nova') @section('content') -
-

Comments

+ +
@if(empty($comments)) -

No comments to show.

+
+

No comments to show.

+
@else -
    - @foreach($comments as $c) -
  • {{ $c }}
  • - @endforeach -
+
+
    + @foreach($comments as $c) +
  • {{ $c }}
  • + @endforeach +
+
@endif
@endsection diff --git a/resources/views/dashboard/favorites.blade.php b/resources/views/dashboard/favorites.blade.php index ac5b727d..e970e6f4 100644 --- a/resources/views/dashboard/favorites.blade.php +++ b/resources/views/dashboard/favorites.blade.php @@ -25,59 +25,32 @@ @vite('resources/js/entry-masonry-gallery.jsx') @endpush -@push('head') - -@endpush - @section('content')
-
- - - -
- - -
-

- My Favourites -

-

- Artworks you saved, displayed in the same gallery layout as Browse. -

- @if($artworks->total() > 0) -
- - {{ number_format($artworks->total()) }} artworks -
- @endif -
-
- - -
+ + + @if($artworks->total() > 0) +
+ + {{ number_format($artworks->total()) }} artworks +
+ @endif +
+
@if($artworks->isEmpty()) diff --git a/resources/views/dashboard/following.blade.php b/resources/views/dashboard/following.blade.php index 9783aea4..aa578ce2 100644 --- a/resources/views/dashboard/following.blade.php +++ b/resources/views/dashboard/following.blade.php @@ -1,94 +1,111 @@ @extends('layouts.nova') @section('content') -
-
-
-

Dashboard

-

People I Follow

-

Creators and members you follow, with quick stats and recent follow time.

-
+
+
+
+ +
- @if($following->isEmpty()) -
-

You are not following anyone yet.

- - - Start following creators - -
- @else - @php - $firstFollow = $following->getCollection()->first(); - $latestFollowedAt = $firstFollow && !empty($firstFollow->followed_at) - ? \Carbon\Carbon::parse($firstFollow->followed_at)->diffForHumans() - : null; - @endphp - -
-
-

Following

-

{{ number_format($following->total()) }}

-
-
-

On this page

-

{{ number_format($following->count()) }}

-
-
-

Last followed

-

{{ $latestFollowedAt ?? '—' }}

-
-
- -
-
- Creator - - Followed -
- -
- @foreach($following as $f) - @php - $displayName = $f->name ?: $f->uname; - @endphp - -
- {{ $displayName }} - + @else + @php + $firstFollow = $following->getCollection()->first(); + $latestFollowedAt = $firstFollow && !empty($firstFollow->followed_at) + ? \Carbon\Carbon::parse($firstFollow->followed_at)->diffForHumans() + : null; + @endphp - +
+
+

Following

+

{{ number_format($following->total()) }}

+
+
+

On this page

+

{{ number_format($following->count()) }}

+
+
+

Last followed

+

{{ $latestFollowedAt ?? '—' }}

+
+
-
- {{ !empty($f->followed_at) ? \Carbon\Carbon::parse($f->followed_at)->diffForHumans() : '—' }} -
- - @endforeach + + +
+ {{ $following->links() }} +
+ @endif +
+
- -
- {{ $following->links() }} -
- @endif +
@endsection diff --git a/resources/views/layouts/nova/content-layout.blade.php b/resources/views/layouts/nova/content-layout.blade.php index 0108f5af..763bf697 100644 --- a/resources/views/layouts/nova/content-layout.blade.php +++ b/resources/views/layouts/nova/content-layout.blade.php @@ -42,17 +42,21 @@ {{-- Minimal hero --}} @if(!empty($center_content)) - {{-- Breadcrumbs --}} - @include('components.breadcrumbs', ['breadcrumbs' => $breadcrumbs ?? collect()]) + @hasSection('page-hero') + @yield('page-hero') + @else + {{-- Breadcrumbs --}} + @include('components.breadcrumbs', ['breadcrumbs' => $breadcrumbs ?? collect()]) -
-

- {{ $hero_title ?? $page_title ?? 'Skinbase' }} -

- @isset($hero_description) -

{{ $hero_description }}

- @endisset -
+
+

+ {{ $hero_title ?? $page_title ?? 'Skinbase' }} +

+ @isset($hero_description) +

{{ $hero_description }}

+ @endisset +
+ @endif
{{-- Page body (centered) --}} @@ -61,17 +65,21 @@ @else
- {{-- Breadcrumbs --}} - @include('components.breadcrumbs', ['breadcrumbs' => $breadcrumbs ?? collect()]) + @hasSection('page-hero') + @yield('page-hero') + @else + {{-- Breadcrumbs --}} + @include('components.breadcrumbs', ['breadcrumbs' => $breadcrumbs ?? collect()]) -
-

- {{ $hero_title ?? $page_title ?? 'Skinbase' }} -

- @isset($hero_description) -

{{ $hero_description }}

- @endisset -
+
+

+ {{ $hero_title ?? $page_title ?? 'Skinbase' }} +

+ @isset($hero_description) +

{{ $hero_description }}

+ @endisset +
+ @endif
{{-- Page body --}} diff --git a/resources/views/layouts/nova/toolbar.blade.php b/resources/views/layouts/nova/toolbar.blade.php index b4fdab32..7fb9e1d8 100644 --- a/resources/views/layouts/nova/toolbar.blade.php +++ b/resources/views/layouts/nova/toolbar.blade.php @@ -75,11 +75,11 @@ @@ -394,8 +397,9 @@
diff --git a/resources/views/news/_article_card.blade.php b/resources/views/news/_article_card.blade.php index e4769e9f..b0920bd4 100644 --- a/resources/views/news/_article_card.blade.php +++ b/resources/views/news/_article_card.blade.php @@ -1,31 +1,37 @@ -{{-- Reusable article card partial --}} -
- @if($article->cover_url) - - {{ $article->title }} - - @endif -
- @if($article->category) - {{ $article->category->name }} - @endif +
+ +
+ @if($article->cover_url) + {{ $article->title }} + @else +
+ @endif +
+
+
-
- - {{ $article->title }} - -
+
+
+ @if($article->category) + {{ $article->category->name }} + @endif + {{ $article->published_at?->format('d M Y') }} +
+ +

+ {{ $article->title }} +

@if($article->excerpt) -

{{ Str::limit($article->excerpt, 100) }}

+

{{ Str::limit(strip_tags((string) $article->excerpt), 135) }}

@endif -
- {{ $article->published_at?->format('d M Y') }} - {{ number_format($article->views) }} +
+ {{ $article->author?->name ?? 'Skinbase' }} + + + {{ number_format((int) $article->views) }} +
-
+
diff --git a/resources/views/news/_sidebar.blade.php b/resources/views/news/_sidebar.blade.php index 46481f02..069fb9e2 100644 --- a/resources/views/news/_sidebar.blade.php +++ b/resources/views/news/_sidebar.blade.php @@ -1,59 +1,58 @@ -{{-- Sidebar partial for news frontend --}} - -{{-- Categories widget --}} @if(!empty($categories) && $categories->isNotEmpty()) -
-
Categories
- +
@endif -{{-- Trending articles --}} @if(!empty($trending) && $trending->isNotEmpty()) -
-
Trending
-
+
+
+ + Trending +
+ +
@endif -{{-- Tags cloud --}} @if(!empty($tags) && $tags->isNotEmpty()) -
-
Tags
-
+
+
+ + Topics +
+
@foreach($tags as $tag) - - {{ $tag->name }} - + #{{ $tag->name }} @endforeach
-
+ @endif -{{-- RSS link --}} - +
+

Stay updated

+ + + RSS Feed + +
diff --git a/resources/views/news/category.blade.php b/resources/views/news/category.blade.php index 52e24983..a1170119 100644 --- a/resources/views/news/category.blade.php +++ b/resources/views/news/category.blade.php @@ -1,30 +1,44 @@ @extends('news.layout', [ 'metaTitle' => $category->name . ' — News', + 'metaDescription' => $category->description ?: ('Announcements in the ' . $category->name . ' category.'), + 'metaCanonical' => route('news.category', $category->slug), ]) @section('news_content') -
-

{{ $category->name }}

- @if($category->description) -

{{ $category->description }}

- @endif +@php + $headerBreadcrumbs = collect([ + (object) ['name' => 'Community', 'url' => route('community.activity')], + (object) ['name' => 'Announcements', 'url' => route('news.index')], + (object) ['name' => $category->name, 'url' => route('news.category', $category->slug)], + ]); +@endphp -
-
-
- @forelse($articles as $article) -
- @include('news._article_card', ['article' => $article]) + + +
+
+
+ @if($articles->isEmpty()) +
No articles in this category yet.
+ @else +
+ @foreach($articles as $article) + @include('news._article_card', ['article' => $article]) + @endforeach
- @empty -
No articles in this category.
- @endforelse -
-
{{ $articles->links() }}
-
-
- @include('news._sidebar', ['categories' => $categories]) -
+
{{ $articles->links() }}
+ @endif + +
@endsection diff --git a/resources/views/news/index.blade.php b/resources/views/news/index.blade.php index 4c020a65..375d3da7 100644 --- a/resources/views/news/index.blade.php +++ b/resources/views/news/index.blade.php @@ -1,69 +1,105 @@ @extends('news.layout', [ 'metaTitle' => config('news.rss_title', 'News'), 'metaDescription' => config('news.rss_description', ''), + 'metaCanonical' => route('news.index'), ]) @section('news_content') -
-
+@php + $headerBreadcrumbs = collect([ + (object) ['name' => 'Community', 'url' => route('community.activity')], + (object) ['name' => 'Announcements', 'url' => route('news.index')], + ]); +@endphp - {{-- Featured article --}} - @if($featured) -
- -
+
@endsection diff --git a/resources/views/news/layout.blade.php b/resources/views/news/layout.blade.php index 11963669..e4709d5b 100644 --- a/resources/views/news/layout.blade.php +++ b/resources/views/news/layout.blade.php @@ -1,14 +1,11 @@ -{{-- - Frontend layout wrapper for the News section. - Extends the main app layout. ---}} -@extends('layouts.app') +@extends('layouts.nova') -@section('title', $metaTitle ?? config('news.rss_title', 'News')) - -@if(isset($metaDescription)) -@section('meta_description', $metaDescription) -@endif +@php + $page_title = $metaTitle ?? config('news.rss_title', 'News'); + $page_meta_description = $metaDescription ?? config('news.rss_description', 'Latest announcements and community updates from Skinbase.'); + $page_canonical = $metaCanonical ?? url()->current(); + $page_robots = $metaRobots ?? 'index,follow'; +@endphp @section('content') @yield('news_content') diff --git a/resources/views/news/show.blade.php b/resources/views/news/show.blade.php index 2a0952a3..e3d07512 100644 --- a/resources/views/news/show.blade.php +++ b/resources/views/news/show.blade.php @@ -1,10 +1,22 @@ @extends('news.layout', [ 'metaTitle' => $article->meta_title ?: $article->title, 'metaDescription' => $article->meta_description ?: Str::limit(strip_tags((string)$article->excerpt), 160), + 'metaCanonical' => route('news.show', $article->slug), ]) @section('news_content') +@php + $headerBreadcrumbs = collect([ + (object) ['name' => 'Community', 'url' => route('community.activity')], + (object) ['name' => 'Announcements', 'url' => route('news.index')], + $article->category + ? (object) ['name' => $article->category->name, 'url' => route('news.category', $article->category->slug)] + : null, + (object) ['name' => $article->title, 'url' => route('news.show', $article->slug)], + ])->filter()->values(); +@endphp + {{-- OpenGraph meta --}} @push('head') @@ -20,94 +32,102 @@ @endif @endpush -
-
-
-
+ + +
+ @if($article->category) + {{ $article->category->name }} + @endif + + + {{ $article->reading_time }} min read + +
+
+
- {{-- Cover image --}} - @if($article->cover_url) - {{ $article->title }} - @endif +
+
+
+ @if($article->cover_url) +
+ {{ $article->title }} +
+ @endif - {{-- Meta --}} -
- @if($article->category) - {{ $article->category->name }} - @endif - {{ $article->author?->name }} - · +
+
+ {{ $article->author?->name ?? 'Skinbase' }} {{ $article->published_at?->format('d M Y') }} - · - {{ $article->reading_time }} min read - · - {{ number_format($article->views) }} + {{ number_format((int) $article->views) }} views
-

{{ $article->title }}

- @if($article->excerpt) -

{{ $article->excerpt }}

+

{{ $article->excerpt }}

@endif -
+
{!! $article->content !!}
- {{-- Tags --}} @if($article->tags->isNotEmpty()) -
- +
@foreach($article->tags as $tag) - {{ $tag->name }} + #{{ $tag->name }} @endforeach
@endif - {{-- Share buttons --}} -
- Share: - - Twitter + - {{-- Forum discussion link --}} @if($article->forum_thread_id) - - {{-- Related articles --}} - @if($related->isNotEmpty()) -
-

Related Articles

-
- @foreach($related as $rel) -
- @include('news._article_card', ['article' => $rel]) -
- @endforeach -
+ @if($related->isNotEmpty()) +
+
+

Related Articles

- @endif +
+ @foreach($related as $rel) + @include('news._article_card', ['article' => $rel]) + @endforeach +
+
+ @endif +
-
{{-- col-lg-8 --}} - -
+
@endsection diff --git a/resources/views/news/tag.blade.php b/resources/views/news/tag.blade.php index 2833ec78..46b47283 100644 --- a/resources/views/news/tag.blade.php +++ b/resources/views/news/tag.blade.php @@ -1,29 +1,44 @@ @extends('news.layout', [ 'metaTitle' => '#' . $tag->name . ' — News', + 'metaDescription' => 'Announcements tagged with ' . $tag->name . '.', + 'metaCanonical' => route('news.tag', $tag->slug), ]) @section('news_content') -
-

- #{{ $tag->name }} -

+@php + $headerBreadcrumbs = collect([ + (object) ['name' => 'Community', 'url' => route('community.activity')], + (object) ['name' => 'Announcements', 'url' => route('news.index')], + (object) ['name' => '#' . $tag->name, 'url' => route('news.tag', $tag->slug)], + ]); +@endphp -
-
-
- @forelse($articles as $article) -
- @include('news._article_card', ['article' => $article]) + + +
+
+
+ @if($articles->isEmpty()) +
No articles are using this tag yet.
+ @else +
+ @foreach($articles as $article) + @include('news._article_card', ['article' => $article]) + @endforeach
- @empty -
No articles with this tag.
- @endforelse -
-
{{ $articles->links() }}
-
-
- @include('news._sidebar', ['categories' => $categories]) -
+
{{ $articles->links() }}
+ @endif + +
@endsection diff --git a/resources/views/web/authors/top.blade.php b/resources/views/web/authors/top.blade.php index 1c8ad924..8a2f2cf5 100644 --- a/resources/views/web/authors/top.blade.php +++ b/resources/views/web/authors/top.blade.php @@ -3,16 +3,29 @@ @section('content') {{-- ── Hero header ── --}} -
-
-
-

Community

-

Top Authors

-

Most popular members ranked by artwork {{ $metric === 'downloads' ? 'downloads' : 'views' }}.

-
+@php + $headerBreadcrumbs = collect([ + (object) ['name' => 'Creators', 'url' => '/creators/top'], + (object) ['name' => 'Top Creators', 'url' => route('creators.top')], + ]); +@endphp - {{-- Metric switcher --}} -
-
+ + {{-- ── Leaderboard ── --}} -
+
@php $offset = ($authors->currentPage() - 1) * $authors->perPage(); $isFirstPage = $authors->currentPage() === 1; @@ -73,15 +86,17 @@
-
+
{{ $author->uname }} -
+
{{ $author->uname ?? 'Unknown' }}
@if (!empty($author->username))
{{ '@' . $author->username }}
@endif -
+
+
+
{{ number_format($author->total ?? 0) }}
diff --git a/resources/views/web/category.blade.php b/resources/views/web/category.blade.php index 3843a639..4d527e12 100644 --- a/resources/views/web/category.blade.php +++ b/resources/views/web/category.blade.php @@ -77,5 +77,4 @@ @endsection @push('scripts') - @endpush diff --git a/resources/views/web/comments/monthly.blade.php b/resources/views/web/comments/monthly.blade.php index 17ea35bb..2b995d69 100644 --- a/resources/views/web/comments/monthly.blade.php +++ b/resources/views/web/comments/monthly.blade.php @@ -1,26 +1,32 @@ @extends('layouts.nova') -@section('content') +@php + $headerBreadcrumbs = collect([ + (object) ['name' => $page_title ?? 'Monthly Top Commentators', 'url' => route('legacy.monthly_commentators')], + ]); +@endphp -{{-- ── Hero header ── --}} -
-
-
-

Community

-

Monthly Top Commentators

-

Members who posted the most comments in the last 30 days.

-
+@section('content') + + Last 30 days -
-
+ + {{-- ── Leaderboard ── --}} -
+
@php $offset = ($rows->currentPage() - 1) * $rows->perPage(); $isFirstPage = $rows->currentPage() === 1; diff --git a/resources/views/web/creators/rising.blade.php b/resources/views/web/creators/rising.blade.php index f5c4bbc2..7f2d6e85 100644 --- a/resources/views/web/creators/rising.blade.php +++ b/resources/views/web/creators/rising.blade.php @@ -2,25 +2,30 @@ @section('content') -
-
-
-

Creators

-

- - Rising Creators -

-

Creators gaining momentum with the most views over the last 90 days.

-
+@php + $headerBreadcrumbs = collect([ + (object) ['name' => 'Creators', 'url' => '/creators/top'], + (object) ['name' => 'Rising Creators', 'url' => route('creators.rising')], + ]); +@endphp + + + Top Creators -
-
+ + -
+
@php $offset = ($creators->currentPage() - 1) * $creators->perPage(); $isFirstPage = $creators->currentPage() === 1; @@ -56,16 +61,18 @@ Recent Views
-
+
{{ $creator->uname }} -
+
{{ $creator->uname ?? 'Unknown' }}
@if($creator->username ?? null)
{{ '@' . $creator->username }}
@endif -
{{ number_format($creator->total ?? 0) }}
+
+
+
{{ number_format($creator->total ?? 0) }}
diff --git a/resources/views/web/daily-uploads.blade.php b/resources/views/web/daily-uploads.blade.php index f663fbf6..df5aa5ce 100644 --- a/resources/views/web/daily-uploads.blade.php +++ b/resources/views/web/daily-uploads.blade.php @@ -1,27 +1,36 @@ @extends('layouts.nova') +@php + $headerBreadcrumbs = collect([ + (object) ['name' => $page_title ?? 'Daily Uploads', 'url' => route('legacy.daily_uploads')], + ]); +@endphp + @section('content') -{{-- ── Hero header ── --}} -
-
-
-

Skinbase

-

Daily Uploads

-

Browse all artworks uploaded on a specific date.

-
- + + + Latest Uploads -
-
+ + {{-- ── Date strip ── --}} -
+
@foreach($dates as $i => $d)