Wire admin studio SSR and search infrastructure
This commit is contained in:
@@ -15,6 +15,7 @@ const baseNavGroups = [
|
||||
label: 'Create',
|
||||
items: [
|
||||
{ label: 'New Artwork', href: '/upload', icon: 'fa-solid fa-cloud-arrow-up' },
|
||||
{ label: 'Upload Queue', href: '/studio/upload-queue', icon: 'fa-solid fa-layer-group' },
|
||||
{ label: 'New Card', href: '/studio/cards/create', icon: 'fa-solid fa-id-card' },
|
||||
{ label: 'New Story', href: '/creator/stories/create', icon: 'fa-solid fa-feather-pointed' },
|
||||
{ label: 'New Collection', href: '/settings/collections/create', icon: 'fa-solid fa-layer-group' },
|
||||
@@ -34,6 +35,7 @@ const baseNavGroups = [
|
||||
label: 'Library',
|
||||
items: [
|
||||
{ label: 'Drafts', href: '/studio/drafts', icon: 'fa-solid fa-file-pen' },
|
||||
{ label: 'Upload Queue', href: '/studio/upload-queue', icon: 'fa-solid fa-list-check' },
|
||||
{ label: 'Scheduled', href: '/studio/scheduled', icon: 'fa-solid fa-calendar-days' },
|
||||
{ label: 'Calendar', href: '/studio/calendar', icon: 'fa-solid fa-calendar-range' },
|
||||
{ label: 'Archived', href: '/studio/archived', icon: 'fa-solid fa-box-archive' },
|
||||
@@ -168,25 +170,40 @@ export default function StudioLayout({ children, title, subtitle, actions }) {
|
||||
const studioGroups = Array.isArray(props.studio_groups) ? props.studio_groups : []
|
||||
const currentGroup = props.studioGroup || null
|
||||
const canManageNews = Boolean(props.auth?.user?.is_admin || props.auth?.user?.is_moderator)
|
||||
const canManageWorlds = canManageNews
|
||||
|
||||
const navGroups = baseNavGroups.map((group) => {
|
||||
if (!canManageNews || group.label !== 'Content') {
|
||||
if ((!canManageNews && !canManageWorlds) || group.label !== 'Content') {
|
||||
return group
|
||||
}
|
||||
|
||||
const extraItems = []
|
||||
|
||||
if (canManageNews) {
|
||||
extraItems.push({ label: 'News', href: '/studio/news', icon: 'fa-solid fa-newspaper' })
|
||||
}
|
||||
|
||||
if (canManageWorlds) {
|
||||
extraItems.push({ label: 'Worlds', href: '/studio/worlds', icon: 'fa-solid fa-globe' })
|
||||
}
|
||||
|
||||
return {
|
||||
...group,
|
||||
items: [
|
||||
...group.items,
|
||||
{ label: 'News', href: '/studio/news', icon: 'fa-solid fa-newspaper' },
|
||||
],
|
||||
items: [...group.items, ...extraItems],
|
||||
}
|
||||
})
|
||||
|
||||
const quickCreateItems = (canManageNews
|
||||
? [...baseQuickCreateItems, { label: 'News Article', href: '/studio/news/create', icon: 'fa-solid fa-newspaper' }]
|
||||
: baseQuickCreateItems
|
||||
).map((item) => {
|
||||
const quickCreatePool = [...baseQuickCreateItems]
|
||||
|
||||
if (canManageNews) {
|
||||
quickCreatePool.push({ label: 'News Article', href: '/studio/news/create', icon: 'fa-solid fa-newspaper' })
|
||||
}
|
||||
|
||||
if (canManageWorlds) {
|
||||
quickCreatePool.push({ label: 'World', href: '/studio/worlds/create', icon: 'fa-solid fa-globe' })
|
||||
}
|
||||
|
||||
const quickCreateItems = quickCreatePool.map((item) => {
|
||||
if (currentGroup?.urls && item.label === 'Artwork') {
|
||||
return { ...item, href: currentGroup.urls?.studio_artworks ? `/upload?group=${currentGroup.slug}` : item.href }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useState } from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import ProfileCoverEditor from './ProfileCoverEditor'
|
||||
import LevelBadge from '../xp/LevelBadge'
|
||||
import XPProgressBar from '../xp/XPProgressBar'
|
||||
import FollowButton from '../social/FollowButton'
|
||||
import FollowersPreview from '../social/FollowersPreview'
|
||||
import MutualFollowersBadge from '../social/MutualFollowersBadge'
|
||||
import { shinyFlagUrl } from '../../utils/flagUrl'
|
||||
|
||||
function formatCompactNumber(value) {
|
||||
const numeric = Number(value ?? 0)
|
||||
@@ -12,11 +14,13 @@ function formatCompactNumber(value) {
|
||||
}
|
||||
|
||||
export default function ProfileHero({ user, profile, isOwner, viewerIsFollowing, followerCount, recentFollowers = [], followContext = null, heroBgUrl, countryName, leaderboardRank, extraActions = null }) {
|
||||
const { props } = usePage()
|
||||
const [following, setFollowing] = useState(viewerIsFollowing)
|
||||
const [count, setCount] = useState(followerCount)
|
||||
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 flagUrl = shinyFlagUrl(profile?.country_code, props?.cdn?.files_url)
|
||||
|
||||
const uname = user.username || user.name || 'Unknown'
|
||||
const displayName = user.name || uname
|
||||
@@ -118,9 +122,9 @@ export default function ProfileHero({ user, profile, isOwner, viewerIsFollowing,
|
||||
{!isOwner ? <MutualFollowersBadge context={followContext} /> : null}
|
||||
{countryName ? (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-slate-300">
|
||||
{profile?.country_code ? (
|
||||
{flagUrl ? (
|
||||
<img
|
||||
src={`/gfx/flags/shiny/24/${encodeURIComponent(String(profile.country_code).toUpperCase())}.png`}
|
||||
src={flagUrl}
|
||||
alt={countryName}
|
||||
className="h-auto w-4 rounded-sm"
|
||||
onError={(event) => { event.target.style.display = 'none' }}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import { shinyFlagUrl } from '../../../utils/flagUrl'
|
||||
|
||||
const SOCIAL_ICONS = {
|
||||
twitter: { icon: 'fa-brands fa-x-twitter', label: 'X / Twitter' },
|
||||
@@ -170,10 +172,12 @@ function SectionCard({ icon, eyebrow, title, children, className = '' }) {
|
||||
* Bio, social links, metadata - replaces old sidebar profile card.
|
||||
*/
|
||||
export default function TabAbout({ user, profile, stats, achievements, artworks, creatorStories, profileComments, socialLinks, countryName, followerCount, recentFollowers, leaderboardRank, groupContributionHistory }) {
|
||||
const { props } = usePage()
|
||||
const uname = user.username || user.name
|
||||
const displayName = user.name || uname
|
||||
const about = profile?.about
|
||||
const website = profile?.website
|
||||
const flagUrl = shinyFlagUrl(profile?.country_code, props?.cdn?.files_url)
|
||||
|
||||
const joinDate = user.created_at
|
||||
? new Date(user.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })
|
||||
@@ -250,9 +254,9 @@ export default function TabAbout({ user, profile, stats, achievements, artworks,
|
||||
{countryName ? (
|
||||
<InfoRow icon="fa-earth-americas" label="Country">
|
||||
<span className="flex items-center gap-2">
|
||||
{profile?.country_code ? (
|
||||
{flagUrl ? (
|
||||
<img
|
||||
src={`/gfx/flags/shiny/24/${encodeURIComponent(String(profile.country_code).toUpperCase())}.png`}
|
||||
src={flagUrl}
|
||||
alt={countryName}
|
||||
className="h-auto w-4 rounded-sm"
|
||||
onError={(e) => { e.target.style.display = 'none' }}
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
@if($countryName)
|
||||
<p class="text-[--sb-muted] text-sm mt-1 flex items-center justify-center sm:justify-start gap-1.5">
|
||||
@if($profile?->country_code)
|
||||
<img src="/gfx/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
|
||||
<img src="{{ rtrim((string) config('cdn.files_url', ''), '/') }}/images/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
|
||||
alt="{{ e($countryName) }}"
|
||||
class="w-5 h-auto rounded-sm inline-block"
|
||||
onerror="this.style.display='none'">
|
||||
@@ -434,7 +434,7 @@
|
||||
<td>Country</td>
|
||||
<td class="flex items-center justify-end gap-1.5">
|
||||
@if($profile?->country_code)
|
||||
<img src="/gfx/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
|
||||
<img src="{{ rtrim((string) config('cdn.files_url', ''), '/') }}/images/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
|
||||
alt="{{ e($countryName) }}"
|
||||
class="w-4 h-auto rounded-sm"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Logo -->
|
||||
<a href="/" class="flex items-center gap-2 pr-2 shrink-0">
|
||||
<img src="/gfx/sb_logo.webp" alt="" width="289" height="100" class="h-9 w-auto rounded-sm shadow-sm object-contain">
|
||||
<img src="https://cdn.skinbase.org/images/sb_logo.webp" alt="" width="289" height="100" class="h-9 w-auto rounded-sm shadow-sm object-contain">
|
||||
<span class="sr-only">Skinbase.org</span>
|
||||
</a>
|
||||
|
||||
@@ -372,26 +372,18 @@
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-upload text-xs text-sb-muted"></i></span>
|
||||
Upload
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeDashboard }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-table-columns text-xs text-sb-muted"></i></span>
|
||||
Dashboard
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/studio/artworks">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-palette text-xs text-sb-muted"></i></span>
|
||||
Studio
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeMyStories }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-book-open text-xs text-sb-muted"></i></span>
|
||||
My Stories
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeDashboardFavorites }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-heart text-xs text-sb-muted"></i></span>
|
||||
My Favorites
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeDashboard }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-table-columns text-xs text-sb-muted"></i></span>
|
||||
Dashboard
|
||||
</a>
|
||||
<a class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.comments.received') }}">
|
||||
<span class="flex items-center gap-3 min-w-0">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-inbox text-xs text-sb-muted"></i></span>
|
||||
<span>Received Comments</span>
|
||||
<span>Comments</span>
|
||||
</span>
|
||||
@if(($receivedCommentsCount ?? 0) > 0)
|
||||
<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>
|
||||
@@ -489,16 +481,19 @@
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/discover/most-downloaded"><i class="fa-solid fa-download w-4 text-center text-sb-muted"></i>Most Downloaded</a>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('downloads.today') }}"><i class="fa-solid fa-arrow-down-short-wide w-4 text-center text-sb-muted"></i>Today Downloads</a>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/discover/on-this-day"><i class="fa-solid fa-calendar-day w-4 text-center text-sb-muted"></i>On This Day</a>
|
||||
@if($skinbaseToolbarCanAuth)
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('discover.for-you') }}"><i class="fa-solid fa-wand-magic-sparkles w-4 text-center"></i>For You</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-1">
|
||||
<button type="button" data-mobile-section-toggle aria-controls="mobileSectionBrowse" aria-expanded="false" class="w-full flex items-center justify-between py-2.5 px-3 rounded-lg text-[11px] font-semibold uppercase tracking-widest text-sb-muted hover:bg-white/5">
|
||||
<span>Browse</span>
|
||||
<span>Explore</span>
|
||||
<i data-mobile-section-icon class="fa-solid fa-chevron-down text-xs transition-transform"></i>
|
||||
</button>
|
||||
<div id="mobileSectionBrowse" data-mobile-section-panel class="hidden mt-0.5 space-y-0.5">
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/browse"><i class="fa-solid fa-border-all w-4 text-center text-sb-muted"></i>All Artworks</a>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/explore"><i class="fa-solid fa-border-all w-4 text-center text-sb-muted"></i>All Artworks</a>
|
||||
@foreach($toolbarContentTypes as $contentType)
|
||||
@php
|
||||
$contentTypeSlug = strtolower((string) $contentType->slug);
|
||||
@@ -506,6 +501,7 @@
|
||||
@endphp
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/{{ $contentTypeSlug }}"><i class="fa-solid {{ $contentTypeIcon }} w-4 text-center text-sb-muted"></i>{{ $contentType->name }}</a>
|
||||
@endforeach
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('categories.index') }}"><i class="fa-solid fa-folder-open w-4 text-center text-sb-muted"></i>Categories</a>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/tags"><i class="fa-solid fa-tags w-4 text-center text-sb-muted"></i>Tags</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -535,7 +531,6 @@
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/creators/rising"><i class="fa-solid fa-arrow-trend-up w-4 text-center text-sb-muted"></i>Rising Creators</a>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/stories"><i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories</a>
|
||||
@if($skinbaseToolbarCanAuth)
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('creator.stories.index') }}"><i class="fa-solid fa-rectangle-list w-4 text-center text-sb-muted"></i>My Stories</a>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.following') }}"><i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user