Files
SkinbaseNova/resources/js/Layouts/AdminLayout.jsx
T
klevze ce0f278bac Add a read-only Google AdSense analytics module for admins.
Connect AdSense over OAuth, sync entities and daily reports locally, and show placement performance in the admin panel without calling the Management API from public pages.
2026-09-20 14:49:48 +02:00

194 lines
8.2 KiB
React

import React, { useState } from 'react'
import { Link, usePage } from '@inertiajs/react'
const buildAdminNavGroups = (isAdmin) => [
{
label: 'Overview',
items: [
{ label: 'Dashboard', href: '/moderation', icon: 'fa-solid fa-gauge-high', exact: true },
{ label: 'Daily Activity', href: '/moderation/activity', icon: 'fa-solid fa-calendar-day' },
{ label: 'Online Users', href: '/moderation/traffic/online', icon: 'fa-solid fa-user-check' },
...(isAdmin ? [{ label: 'AdSense Analytics', href: '/admin/adsense', icon: 'fa-solid fa-chart-column' }] : []),
],
},
{
label: 'People',
items: [
{ label: 'All Users', href: '/moderation/users', icon: 'fa-solid fa-users' },
{ label: 'Staff', href: '/moderation/users?role=admin', icon: 'fa-solid fa-shield-halved' },
{ label: 'Moderators', href: '/moderation/users?role=moderator', icon: 'fa-solid fa-user-shield' },
{ label: 'Staff Applications', href: '/moderation/staff-applications', icon: 'fa-solid fa-user-check' },
],
},
{
label: 'Content',
items: [
{ label: 'Stories', href: '/moderation/stories', icon: 'fa-solid fa-feather-pointed' },
{ label: 'Artworks', href: '/moderation/artworks', icon: 'fa-solid fa-images' },
{ label: 'Enhance Jobs', href: '/moderation/enhance', icon: 'fa-solid fa-up-right-and-down-left-from-center' },
{ label: 'Featured Artworks', href: '/moderation/artworks/featured', icon: 'fa-solid fa-star' },
{ label: 'Web Stories', href: '/moderation/web-stories', icon: 'fa-solid fa-book-open-reader' },
{ label: 'Homepage Announcements', href: '/moderation/homepage/announcements', icon: 'fa-solid fa-bullhorn' },
{ label: 'Upload Queue', href: '/moderation/uploads', icon: 'fa-solid fa-cloud-arrow-up' },
{ label: 'Username Queue', href: '/moderation/usernames/moderation', icon: 'fa-solid fa-id-badge' },
],
},
{
label: 'Academy',
items: [
{ label: 'Academy Dashboard', href: '/moderation/academy/dashboard', icon: 'fa-solid fa-graduation-cap' },
{ label: 'Academy Billing', href: '/moderation/academy/billing', icon: 'fa-solid fa-credit-card' },
{ label: 'Academy Analytics', href: '/moderation/academy/analytics', icon: 'fa-solid fa-chart-line' },
{ label: 'Academy Courses', href: '/moderation/academy/courses', icon: 'fa-solid fa-road' },
{ label: 'Academy Lessons', href: '/moderation/academy/lessons', icon: 'fa-solid fa-book-open' },
{ label: 'Academy Prompts', href: '/moderation/academy/prompts', icon: 'fa-solid fa-wand-magic-sparkles' },
{ label: 'Academy Challenges', href: '/moderation/academy/challenges', icon: 'fa-solid fa-trophy' },
],
},
{
label: 'System',
items: [
...(isAdmin ? [{ label: 'Auth Audit', href: '/moderation/auth-audit', icon: 'fa-solid fa-user-shield' }] : []),
...(isAdmin ? [{ label: 'Security Report', href: '/moderation/system/security-report', icon: 'fa-solid fa-shield-virus' }] : []),
{ label: 'Settings', href: '/moderation/settings', icon: 'fa-solid fa-gear' },
],
},
]
function NavLink({ item, active }) {
const cls = `flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ${
active
? 'bg-rose-500/20 text-rose-300 shadow-sm shadow-rose-500/10'
: 'text-slate-400 hover:text-white hover:bg-white/5'
}`
// For some moderation surfaces (traffic/online) we prefer a full-page
// navigation so the server-rendered blade view opens as its own page
// instead of being handled by Inertia or opened inline.
if (item.href === '/moderation/traffic/online') {
return (
<a href={item.href} className={cls}>
<i className={`${item.icon} w-5 text-center text-base`} />
<span>{item.label}</span>
</a>
)
}
return (
<Link href={item.href} className={cls}>
<i className={`${item.icon} w-5 text-center text-base`} />
<span>{item.label}</span>
</Link>
)
}
function Sidebar({ pathname, isAdmin }) {
const adminNavGroups = buildAdminNavGroups(isAdmin)
const isActive = (item) => {
if (item.exact) return pathname === item.href
return pathname.startsWith(item.href.split('?')[0])
}
return (
<aside className="nova-scrollbar flex h-full w-64 flex-col overflow-y-auto border-r border-white/[0.07] bg-[rgba(10,14,22,0.98)] px-3 py-6">
{/* Brand */}
<div className="mb-8 px-3">
<Link href="/moderation" className="flex items-center gap-2.5">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-rose-500/20">
<i className="fa-solid fa-shield-halved text-sm text-rose-400" />
</span>
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-rose-400/80">Skinbase</p>
<p className="text-sm font-bold text-white">Admin Panel</p>
</div>
</Link>
</div>
{/* Nav groups */}
<nav className="flex-1 space-y-6">
{adminNavGroups.map((group) => (
<div key={group.label}>
<p className="mb-1.5 px-4 text-[10px] font-bold uppercase tracking-[0.2em] text-slate-600">
{group.label}
</p>
<div className="space-y-0.5">
{group.items.map((item) => (
<NavLink key={item.href} item={item} active={isActive(item)} />
))}
</div>
</div>
))}
</nav>
{/* Footer links */}
<div className="mt-6 space-y-0.5 border-t border-white/[0.06] pt-4">
<Link
href="/studio"
className="flex items-center gap-3 rounded-xl px-4 py-2.5 text-sm text-slate-500 transition hover:bg-white/5 hover:text-slate-300"
>
<i className="fa-solid fa-arrow-left w-5 text-center text-base" />
<span>Back to Studio</span>
</Link>
</div>
</aside>
)
}
export default function AdminLayout({ children, title, subtitle }) {
const { url, props } = usePage()
const [mobileOpen, setMobileOpen] = useState(false)
const pathname = url.split('?')[0]
const currentUserIsAdmin = Boolean(props.auth?.user?.is_admin)
return (
<div className="flex min-h-screen bg-[radial-gradient(ellipse_at_top,_rgba(239,68,68,0.08),_transparent_40%),linear-gradient(180deg,#060a12_0%,#020409_100%)]">
{/* Desktop sidebar */}
<div className="hidden lg:flex lg:w-64 lg:flex-shrink-0">
<div className="fixed inset-y-0 left-0 w-64">
<Sidebar pathname={pathname} isAdmin={currentUserIsAdmin} />
</div>
</div>
{/* Mobile header */}
<div className="fixed inset-x-0 top-0 z-40 flex items-center justify-between border-b border-white/10 bg-[rgba(10,14,22,0.97)] px-4 py-3 backdrop-blur-xl lg:hidden">
<Link href="/moderation" className="flex items-center gap-2">
<i className="fa-solid fa-shield-halved text-rose-400" />
<span className="text-sm font-bold text-white">Admin Panel</span>
</Link>
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="rounded-full border border-white/10 p-2 text-slate-400 hover:text-white"
aria-label="Toggle navigation"
>
<i className={mobileOpen ? 'fa-solid fa-xmark' : 'fa-solid fa-bars'} />
</button>
</div>
{/* Mobile drawer */}
{mobileOpen && (
<div className="fixed inset-0 z-30 lg:hidden">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setMobileOpen(false)} />
<div className="absolute left-0 top-0 h-full w-72 pt-14">
<Sidebar pathname={pathname} isAdmin={currentUserIsAdmin} />
</div>
</div>
)}
{/* Main content */}
<div className="flex flex-1 flex-col lg:pl-8">
<main className="flex-1 px-6 py-8 pt-20 lg:pt-8">
{(title || subtitle) && (
<div className="mb-8">
{title && <h1 className="text-2xl font-bold text-white">{title}</h1>}
{subtitle && <p className="mt-1 text-sm text-slate-400">{subtitle}</p>}
</div>
)}
{children}
</main>
</div>
</div>
)
}