chore: commit current workspace changes
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link, usePage } from '@inertiajs/react'
|
||||
|
||||
const adminNavGroups = [
|
||||
const buildAdminNavGroups = (isAdmin) => [
|
||||
{
|
||||
label: 'Overview',
|
||||
items: [
|
||||
@@ -31,6 +31,7 @@ const adminNavGroups = [
|
||||
{
|
||||
label: 'System',
|
||||
items: [
|
||||
...(isAdmin ? [{ label: 'Auth Audit', href: '/moderation/auth-audit', icon: 'fa-solid fa-user-shield' }] : []),
|
||||
{ label: 'Settings', href: '/moderation/settings', icon: 'fa-solid fa-gear' },
|
||||
],
|
||||
},
|
||||
@@ -51,7 +52,9 @@ function NavLink({ item, active }) {
|
||||
)
|
||||
}
|
||||
|
||||
function Sidebar({ pathname }) {
|
||||
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])
|
||||
@@ -103,9 +106,10 @@ function Sidebar({ pathname }) {
|
||||
}
|
||||
|
||||
export default function AdminLayout({ children, title, subtitle }) {
|
||||
const { url } = usePage()
|
||||
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%)]">
|
||||
@@ -113,7 +117,7 @@ export default function AdminLayout({ children, title, subtitle }) {
|
||||
{/* 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} />
|
||||
<Sidebar pathname={pathname} isAdmin={currentUserIsAdmin} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,13 +141,13 @@ export default function AdminLayout({ children, title, subtitle }) {
|
||||
<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} />
|
||||
<Sidebar pathname={pathname} isAdmin={currentUserIsAdmin} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 flex-col lg:pl-64">
|
||||
<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">
|
||||
|
||||
232
resources/js/Pages/Admin/AuthAudit.jsx
Normal file
232
resources/js/Pages/Admin/AuthAudit.jsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React from 'react'
|
||||
import { Head, router } from '@inertiajs/react'
|
||||
import AdminLayout from '../../Layouts/AdminLayout'
|
||||
|
||||
const EVENT_LABELS = {
|
||||
login: 'Login',
|
||||
register: 'Register',
|
||||
forgot_password: 'Forgot password',
|
||||
reset_password: 'Reset password',
|
||||
}
|
||||
|
||||
const STATUS_BADGES = {
|
||||
success: 'border-emerald-400/20 bg-emerald-400/10 text-emerald-200',
|
||||
failed: 'border-rose-400/20 bg-rose-400/10 text-rose-200',
|
||||
}
|
||||
|
||||
function formatTimestamp(value) {
|
||||
if (!value) {
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'medium',
|
||||
}).format(new Date(value))
|
||||
}
|
||||
|
||||
function formatLabel(value) {
|
||||
if (!value) {
|
||||
return 'Not recorded'
|
||||
}
|
||||
|
||||
return value
|
||||
.replaceAll('_', ' ')
|
||||
.replace(/\b\w/g, (match) => match.toUpperCase())
|
||||
}
|
||||
|
||||
function SummaryCard({ label, value, tone = 'slate' }) {
|
||||
const tones = {
|
||||
slate: 'border-white/[0.07] bg-white/[0.02] text-white',
|
||||
rose: 'border-rose-400/15 bg-rose-500/10 text-rose-100',
|
||||
sky: 'border-sky-400/15 bg-sky-500/10 text-sky-100',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded-2xl border p-5 ${tones[tone] ?? tones.slate}`}>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">{label}</p>
|
||||
<p className="mt-3 text-3xl font-semibold tracking-tight">{value}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AuthAudit({ logs, filters, eventOptions, statusOptions }) {
|
||||
const items = logs?.data ?? []
|
||||
const failedCount = items.filter((entry) => entry.status === 'failed').length
|
||||
const uniqueIpCount = new Set(items.map((entry) => entry.ip).filter(Boolean)).size
|
||||
|
||||
const handleSearch = (event) => {
|
||||
event.preventDefault()
|
||||
const search = event.target.elements.search.value
|
||||
|
||||
router.get('/moderation/auth-audit', {
|
||||
search,
|
||||
event: filters.event,
|
||||
status: filters.status,
|
||||
}, {
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
})
|
||||
}
|
||||
|
||||
const handleFilterChange = (key, value) => {
|
||||
router.get('/moderation/auth-audit', {
|
||||
search: filters.search,
|
||||
event: key === 'event' ? value : filters.event,
|
||||
status: key === 'status' ? value : filters.status,
|
||||
}, {
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout title="Auth Audit" subtitle="Review login, registration, forgot-password, and reset-password activity with IPs, timestamps, status, and failure reasons.">
|
||||
<Head title="Admin · Auth Audit" />
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
<SummaryCard label="Visible records" value={items.length.toLocaleString()} tone="slate" />
|
||||
<SummaryCard label="Failures on page" value={failedCount.toLocaleString()} tone="rose" />
|
||||
<SummaryCard label="Unique IPs on page" value={uniqueIpCount.toLocaleString()} tone="sky" />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-5">
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
|
||||
<form onSubmit={handleSearch} className="flex flex-1 flex-col gap-3 md:flex-row">
|
||||
<label className="flex-1">
|
||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Search</span>
|
||||
<input
|
||||
name="search"
|
||||
defaultValue={filters.search}
|
||||
placeholder="Email, username, IP, or failure reason"
|
||||
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white placeholder:text-slate-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" className="rounded-2xl bg-rose-500/80 px-5 py-3 text-sm font-semibold text-white transition hover:bg-rose-500">
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:min-w-[26rem]">
|
||||
<label>
|
||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Event</span>
|
||||
<select
|
||||
value={filters.event}
|
||||
onChange={(event) => handleFilterChange('event', event.target.value)}
|
||||
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
||||
>
|
||||
{eventOptions.map((option) => (
|
||||
<option key={option.value} value={option.value} className="bg-slate-950 text-white">
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Status</span>
|
||||
<select
|
||||
value={filters.status}
|
||||
onChange={(event) => handleFilterChange('status', event.target.value)}
|
||||
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
||||
>
|
||||
{statusOptions.map((option) => (
|
||||
<option key={option.value} value={option.value} className="bg-slate-950 text-white">
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 overflow-hidden rounded-[28px] border border-white/[0.07] bg-white/[0.02]">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[980px] text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.07] text-left text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
|
||||
<th className="px-5 py-4">When</th>
|
||||
<th className="px-5 py-4">Event</th>
|
||||
<th className="px-5 py-4">Status</th>
|
||||
<th className="px-5 py-4">Identifier</th>
|
||||
<th className="px-5 py-4">User</th>
|
||||
<th className="px-5 py-4">IP</th>
|
||||
<th className="px-5 py-4">Reason</th>
|
||||
<th className="px-5 py-4">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.05]">
|
||||
{items.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-5 py-12 text-center text-slate-500">No auth audit records matched the current filters.</td>
|
||||
</tr>
|
||||
) : items.map((entry) => (
|
||||
<tr key={entry.id} className="align-top transition hover:bg-white/[0.025]">
|
||||
<td className="px-5 py-4 text-slate-300">{formatTimestamp(entry.created_at)}</td>
|
||||
<td className="px-5 py-4 text-white">{EVENT_LABELS[entry.event_type] ?? formatLabel(entry.event_type)}</td>
|
||||
<td className="px-5 py-4">
|
||||
<span className={`inline-flex rounded-full border px-2.5 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${STATUS_BADGES[entry.status] ?? 'border-white/10 bg-white/[0.04] text-white/70'}`}>
|
||||
{entry.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-slate-300">{entry.identifier || 'Not recorded'}</td>
|
||||
<td className="px-5 py-4">
|
||||
{entry.user ? (
|
||||
<div>
|
||||
<p className="font-medium text-white">{entry.user.name}</p>
|
||||
<p className="text-xs text-slate-500">{entry.user.username ? `@${entry.user.username}` : entry.user.email}</p>
|
||||
</div>
|
||||
) : <span className="text-slate-500">Unknown user</span>}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-slate-300">{entry.ip || 'Unknown'}</td>
|
||||
<td className="px-5 py-4 text-slate-300">{formatLabel(entry.reason)}</td>
|
||||
<td className="px-5 py-4">
|
||||
<details className="group w-72 max-w-full">
|
||||
<summary className="cursor-pointer list-none text-sm font-medium text-sky-200 transition hover:text-sky-100">
|
||||
View payload
|
||||
</summary>
|
||||
<div className="mt-3 space-y-3 rounded-2xl border border-white/10 bg-black/20 p-4 text-xs text-slate-300">
|
||||
<div>
|
||||
<p className="font-semibold uppercase tracking-[0.16em] text-slate-500">User agent</p>
|
||||
<p className="mt-1 break-words leading-5 text-slate-300">{entry.user_agent || 'Not recorded'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold uppercase tracking-[0.16em] text-slate-500">Metadata</p>
|
||||
<pre className="mt-1 overflow-x-auto whitespace-pre-wrap break-words rounded-xl bg-slate-950/70 p-3 text-[11px] leading-5 text-slate-300">{JSON.stringify(entry.metadata || {}, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{logs?.last_page > 1 ? (
|
||||
<div className="flex items-center justify-between border-t border-white/[0.06] px-5 py-4">
|
||||
<p className="text-xs text-slate-500">
|
||||
Showing {logs.from}–{logs.to} of {logs.total} audit records
|
||||
</p>
|
||||
<div className="flex gap-1">
|
||||
{logs.links.map((link, index) => (
|
||||
link.url ? (
|
||||
<button
|
||||
key={`${link.label}-${index}`}
|
||||
type="button"
|
||||
onClick={() => router.get(link.url, {}, { preserveScroll: true, preserveState: true })}
|
||||
className={`rounded-lg px-3 py-1.5 text-xs transition ${link.active ? 'bg-rose-500/20 font-semibold text-rose-300' : 'text-slate-500 hover:bg-white/[0.06] hover:text-white'}`}
|
||||
dangerouslySetInnerHTML={{ __html: link.label }}
|
||||
/>
|
||||
) : (
|
||||
<span key={`${link.label}-${index}`} className="rounded-lg px-3 py-1.5 text-xs text-slate-700" dangerouslySetInnerHTML={{ __html: link.label }} />
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
@@ -151,7 +151,7 @@ export default function AiBiographyAdmin() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 pb-16 pt-8 sm:px-6 lg:px-8">
|
||||
<div className="w-full pb-16 pt-8">
|
||||
<Head title="AI Biography Review" />
|
||||
|
||||
<section className="rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(125,211,252,0.2),transparent_32%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.9))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]">
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function ArtworkMaturityQueue() {
|
||||
], [stats])
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 pb-16 pt-8 sm:px-6 lg:px-8">
|
||||
<div className="w-full pb-16 pt-8">
|
||||
<Head title="Artwork Maturity Queue" />
|
||||
|
||||
<section className="rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.16),transparent_36%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]">
|
||||
|
||||
@@ -7,6 +7,11 @@ const pages = {
|
||||
'!./Pages/Help/**/__tests__/**',
|
||||
'!./Pages/Help/**/*.test.jsx',
|
||||
]),
|
||||
...import.meta.glob([
|
||||
'./Pages/Profile/**/*.jsx',
|
||||
'!./Pages/Profile/**/__tests__/**',
|
||||
'!./Pages/Profile/**/*.test.jsx',
|
||||
]),
|
||||
...import.meta.glob([
|
||||
'./Pages/Collection/**/*.jsx',
|
||||
'!./Pages/Collection/**/__tests__/**',
|
||||
|
||||
@@ -57,6 +57,7 @@ export default function Topbar({ user = null }) {
|
||||
<div className="border-t border-neutral-700" />
|
||||
<a href={user.uploadUrl} className="block px-4 py-2 text-sm hover:bg-white/5">Upload</a>
|
||||
<a href="/studio/artworks" className="block px-4 py-2 text-sm hover:bg-white/5">Studio</a>
|
||||
{user.moderationUrl ? <a href={user.moderationUrl} className="block px-4 py-2 text-sm hover:bg-white/5">Moderation</a> : null}
|
||||
<a href="/dashboard" className="block px-4 py-2 text-sm hover:bg-white/5">Dashboard</a>
|
||||
<div className="border-t border-neutral-700" />
|
||||
<a href="/logout" className="block px-4 py-2 text-sm text-red-400 hover:bg-white/5"
|
||||
|
||||
@@ -13,6 +13,7 @@ function mount() {
|
||||
username: container.dataset.username || '',
|
||||
avatarUrl: container.dataset.avatarUrl || null,
|
||||
uploadUrl: container.dataset.uploadUrl || '/upload',
|
||||
moderationUrl: container.dataset.moderationUrl || null,
|
||||
}
|
||||
: null
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { mountInertiaRoot } from './bootstrap'
|
||||
import React from 'react'
|
||||
import { createInertiaApp } from '@inertiajs/react'
|
||||
import ProfileShow from './Pages/Profile/ProfileShow'
|
||||
import ProfileGallery from './Pages/Profile/ProfileGallery'
|
||||
import CollectionShow from './Pages/Collection/CollectionShow'
|
||||
import CollectionSeriesShow from './Pages/Collection/CollectionSeriesShow'
|
||||
import CollectionManage from './Pages/Collection/CollectionManage'
|
||||
import CollectionFeaturedIndex from './Pages/Collection/CollectionFeaturedIndex'
|
||||
import SavedCollections from './Pages/Collection/SavedCollections'
|
||||
|
||||
const pages = {
|
||||
'Profile/ProfileShow': ProfileShow,
|
||||
'Profile/ProfileGallery': ProfileGallery,
|
||||
'Collection/CollectionShow': CollectionShow,
|
||||
'Collection/CollectionSeriesShow': CollectionSeriesShow,
|
||||
'Collection/CollectionManage': CollectionManage,
|
||||
'Collection/CollectionFeaturedIndex': CollectionFeaturedIndex,
|
||||
'Collection/SavedCollections': SavedCollections,
|
||||
}
|
||||
|
||||
createInertiaApp({
|
||||
resolve: (name) => pages[name],
|
||||
setup({ el, App, props }) {
|
||||
mountInertiaRoot(el, App, props)
|
||||
},
|
||||
})
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Verify your email</title>
|
||||
<title>Welcome to Skinbase</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:20px;background:#0b0f14;font-family:system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial;color:#e6eef6;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||
@@ -17,19 +17,30 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:24px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0));">
|
||||
<p style="margin:0 0 12px;color:#cbd5e1;">Welcome to {{ config('app.name', 'Skinbase') }} — thanks for signing up.</p>
|
||||
<p style="margin:0 0 18px;color:#cbd5e1;">Please verify your email to continue account setup.</p>
|
||||
<p style="margin:0 0 16px;color:#e5edf5;">Hello,</p>
|
||||
<p style="margin:0 0 16px;color:#cbd5e1;">Welcome to Skinbase.</p>
|
||||
<p style="margin:0 0 20px;color:#cbd5e1;">Please confirm your email address to activate your Skinbase account. This helps us protect your account and keep the Skinbase community safe.</p>
|
||||
|
||||
<div style="text-align:center;margin:20px 0;">
|
||||
<a href="{{ $verificationUrl }}" style="display:inline-block;padding:12px 20px;background:#0ea5a9;color:#06121a;text-decoration:none;border-radius:8px;font-weight:600;">Verify Email</a>
|
||||
<a href="{{ $verificationUrl }}" style="display:inline-block;padding:12px 20px;background:#0ea5a9;color:#06121a;text-decoration:none;border-radius:8px;font-weight:700;">Confirm Email Address</a>
|
||||
</div>
|
||||
|
||||
<p style="margin:0 0 8px;color:#9fb0c8;font-size:13px;">This link expires in {{ $expiresInHours }} hours.</p>
|
||||
<p style="margin:12px 0 0;color:#9fb0c8;font-size:13px;">Need help? Contact support: <a href="{{ $supportUrl }}" style="color:#8bd0d3;">{{ $supportUrl }}</a></p>
|
||||
<p style="margin:0 0 16px;color:#cbd5e1;">If you did not create a Skinbase account, you can safely ignore this email. No account will be activated unless this email address is confirmed.</p>
|
||||
<p style="margin:0 0 20px;color:#cbd5e1;">Regards,<br>The Skinbase Team</p>
|
||||
|
||||
<p style="margin:0 0 8px;color:#9fb0c8;font-size:13px;">If the button does not work, copy and paste this link into your browser:</p>
|
||||
<p style="margin:0 0 24px;color:#8bd0d3;font-size:13px;word-break:break-all;">
|
||||
<a href="{{ $verificationUrl }}" style="color:#8bd0d3;">{{ $verificationUrl }}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 24px;background:#040607;border-top:1px solid #0e1113;text-align:center;color:#6b7280;font-size:12px;">© {{ date('Y') }} {{ config('app.name', 'Skinbase') }}. All rights reserved.</td>
|
||||
<td style="padding:16px 24px;background:#040607;border-top:1px solid #0e1113;text-align:center;color:#9ca3af;font-size:12px;line-height:1.6;">
|
||||
<div style="margin:0 0 6px;color:#e5edf5;font-weight:600;">Skinbase</div>
|
||||
<div style="margin:0 0 12px;">Digital art, wallpapers, skins, photography, and creative customization.</div>
|
||||
<div style="margin:0 0 6px;">You received this email because someone created a Skinbase account using this email address.</div>
|
||||
<div style="margin:0;">© 2026 Skinbase. All rights reserved.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@php
|
||||
use App\Banner;
|
||||
$src = $sourceArtwork;
|
||||
$useUnifiedSeo = true;
|
||||
$useUnifiedSeo = false;
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
$deferWebManifest = request()->routeIs('index');
|
||||
$isInertiaPage = isset($page) && is_array($page);
|
||||
$shouldRenderBladeSeo = ($useUnifiedSeo ?? false) && (($renderBladeSeo ?? false) || ! $isInertiaPage);
|
||||
$novaViteEntries = [
|
||||
$novaCssEntries = [
|
||||
'resources/css/app.css',
|
||||
'resources/css/nova-grid.css',
|
||||
'resources/scss/nova.scss',
|
||||
];
|
||||
$novaViteEntries = [
|
||||
'resources/js/nova.js',
|
||||
];
|
||||
|
||||
@@ -54,6 +56,9 @@
|
||||
@if(!$deferWebManifest)
|
||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||
@endif
|
||||
@foreach($novaCssEntries as $novaCssEntry)
|
||||
<link rel="stylesheet" href="{{ Vite::asset($novaCssEntry) }}">
|
||||
@endforeach
|
||||
@vite($novaViteEntries)
|
||||
<script>
|
||||
window.SKINBASE_LIMITS = Object.assign({}, window.SKINBASE_LIMITS || {}, {
|
||||
@@ -255,6 +260,7 @@
|
||||
data-username="{{ Auth::user()->username ?? '' }}"
|
||||
data-avatar-url="{{ \App\Support\AvatarUrl::forUser((int) Auth::id(), optional(Auth::user()->profile)->avatar_hash, 64) }}"
|
||||
data-upload-url="{{ Route::has('upload') ? route('upload') : '/upload' }}"
|
||||
data-moderation-url="{{ in_array(strtolower((string) (Auth::user()->role ?? '')), ['admin', 'moderator'], true) ? '/moderation' : '' }}"
|
||||
@endif
|
||||
></div>
|
||||
@include('layouts.nova.toolbar')
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
$routeUpload = Route::has('upload') ? route('upload') : '/upload';
|
||||
$routeDashboard = Route::has('dashboard') ? route('dashboard') : '/dashboard';
|
||||
$routeMyArtworks = Route::has('studio.artworks') ? route('studio.artworks') : '/studio/artworks';
|
||||
$routeModeration = '/moderation';
|
||||
$routeMyStories = Route::has('creator.stories.index') ? route('creator.stories.index') : '/creator/stories';
|
||||
$routeWriteStory = Route::has('creator.stories.create') ? route('creator.stories.create') : '/creator/stories/create';
|
||||
$routeDashboardFavorites = Route::has('dashboard.favorites') ? route('dashboard.favorites') : '/dashboard/favorites';
|
||||
@@ -394,6 +395,12 @@
|
||||
<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>
|
||||
@if(in_array(strtolower((string) (Auth::user()->role ?? '')), ['admin', 'moderator'], true))
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeModeration }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-user-shield text-xs text-sb-muted"></i></span>
|
||||
Moderation
|
||||
</a>
|
||||
@endif
|
||||
<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
|
||||
@@ -423,13 +430,6 @@
|
||||
Settings
|
||||
</a>
|
||||
|
||||
@if(in_array(strtolower((string) (Auth::user()->role ?? '')), ['admin', 'moderator'], true) && \Illuminate\Support\Facades\Route::has('admin.usernames.moderation'))
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('admin.usernames.moderation') }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-user-shield text-xs text-sb-muted"></i></span>
|
||||
Moderation
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<div class="border-t border-panel mt-1 mb-1"></div>
|
||||
<form method="POST" action="{{ route('logout') }}" class="mb-1">
|
||||
@csrf
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
$commentsCollection = $comments ?? collect();
|
||||
$commentsCount = $commentsCount ?? $commentsCollection->count();
|
||||
$viewer = auth()->user();
|
||||
$errors = $errors ?? new \Illuminate\Support\ViewErrorBag();
|
||||
@endphp
|
||||
|
||||
@if($isPreview)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@push('head')
|
||||
@vite(['resources/js/profile.jsx'])
|
||||
@vite(['resources/js/collections.jsx'])
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||
<style>
|
||||
/* Ensure profile tab bar does not hide behind the main navbar */
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
$hero_description = "We're always grateful for volunteers who want to help.";
|
||||
$center_content = true;
|
||||
$center_max = '3xl';
|
||||
$errors = $errors ?? new \Illuminate\Support\ViewErrorBag();
|
||||
@endphp
|
||||
|
||||
@section('page-content')
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
$communityActivityManifestPath = public_path('build/manifest.json');
|
||||
$communityActivityManifest = is_file($communityActivityManifestPath)
|
||||
? json_decode((string) file_get_contents($communityActivityManifestPath), true)
|
||||
: null;
|
||||
$communityActivityViteReady = is_array($communityActivityManifest)
|
||||
&& array_key_exists('resources/js/Pages/Community/CommunityActivityPage.jsx', $communityActivityManifest);
|
||||
|
||||
$initialFilterLabel = match (($initialFilter ?? 'all')) {
|
||||
'comments' => 'Comments',
|
||||
@@ -243,6 +249,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@vite(['resources/js/Pages/Community/CommunityActivityPage.jsx'])
|
||||
@if ($communityActivityViteReady)
|
||||
@vite(['resources/js/Pages/Community/CommunityActivityPage.jsx'])
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
|
||||
Reference in New Issue
Block a user