optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
import React from 'react'
import { Link, usePage } from '@inertiajs/react'
import StudioLayout from '../../Layouts/StudioLayout'
const kpiItems = [
{ key: 'views', label: 'Views', icon: 'fa-eye', color: 'text-emerald-400' },
{ key: 'likes', label: 'Likes', icon: 'fa-heart', color: 'text-pink-400' },
{ key: 'saves', label: 'Saves', icon: 'fa-bookmark', color: 'text-amber-400' },
{ key: 'remixes', label: 'Remixes', icon: 'fa-code-branch', color: 'text-cyan-400' },
{ key: 'comments', label: 'Comments', icon: 'fa-comment', color: 'text-blue-400' },
{ key: 'challenge_entries', label: 'Challenges', icon: 'fa-trophy', color: 'text-violet-400' },
]
const secondaryItems = [
{ key: 'favorites', label: 'Favorites', icon: 'fa-star' },
{ key: 'shares', label: 'Shares', icon: 'fa-share-nodes' },
{ key: 'downloads', label: 'Downloads', icon: 'fa-download' },
]
export default function StudioCardAnalytics() {
const { props } = usePage()
const { card, analytics } = props
return (
<StudioLayout title={`Analytics: ${card?.title || 'Nova Card'}`}>
<Link href="/studio/cards" className="mb-6 inline-flex items-center gap-2 text-sm text-slate-400 transition-colors hover:text-white">
<i className="fa-solid fa-arrow-left" />
Back to Cards
</Link>
<div className="mb-8 flex items-center gap-4 rounded-2xl border border-white/10 bg-nova-900/60 p-4">
{card?.preview_url ? <img src={card.preview_url} alt={card.title} className="h-20 w-20 rounded-xl object-cover bg-nova-800" /> : null}
<div>
<h2 className="text-lg font-bold text-white">{card?.title}</h2>
<p className="mt-1 text-xs text-slate-500">/{card?.slug}</p>
<p className="mt-2 text-xs uppercase tracking-[0.18em] text-slate-400">{card?.status} {card?.visibility}</p>
</div>
</div>
<div className="mb-8 grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-6">
{kpiItems.map((item) => (
<div key={item.key} className="rounded-2xl border border-white/10 bg-nova-900/60 p-5">
<div className="mb-2 flex items-center gap-2">
<i className={`fa-solid ${item.icon} ${item.color}`} />
<span className="text-xs font-medium uppercase tracking-wider text-slate-400">{item.label}</span>
</div>
<p className="text-2xl font-bold tabular-nums text-white">{(analytics?.[item.key] ?? 0).toLocaleString()}</p>
</div>
))}
</div>
<div className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
<div className="rounded-2xl border border-white/10 bg-nova-900/40 p-6">
<h3 className="mb-4 text-sm font-semibold uppercase tracking-[0.18em] text-slate-300">Ranking signals</h3>
<div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div className="text-xs uppercase tracking-[0.18em] text-slate-400">Trending score</div>
<div className="mt-2 text-3xl font-bold tabular-nums text-white">{Number(analytics?.trending_score ?? 0).toFixed(2)}</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div className="text-xs uppercase tracking-[0.18em] text-slate-400">Last engaged</div>
<div className="mt-2 text-sm text-white">{analytics?.last_engaged_at || 'No activity yet'}</div>
</div>
</div>
</div>
<div className="rounded-2xl border border-white/10 bg-nova-900/40 p-6">
<h3 className="mb-4 text-sm font-semibold uppercase tracking-[0.18em] text-slate-300">Secondary metrics</h3>
<div className="space-y-3">
{secondaryItems.map((item) => (
<div key={item.key} className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3">
<div className="flex items-center gap-2 text-sm text-slate-300">
<i className={`fa-solid ${item.icon} text-slate-500`} />
{item.label}
</div>
<div className="text-base font-semibold tabular-nums text-white">{(analytics?.[item.key] ?? 0).toLocaleString()}</div>
</div>
))}
</div>
</div>
</div>
</StudioLayout>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,134 @@
import React from 'react'
import { Head, Link, usePage } from '@inertiajs/react'
import StudioLayout from '../../Layouts/StudioLayout'
import NovaCardCanvasPreview from '../../components/nova-cards/NovaCardCanvasPreview'
function requestJson(url, { method = 'POST' } = {}) {
return fetch(url, {
method,
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
'X-Requested-With': 'XMLHttpRequest',
},
}).then(async (response) => {
const payload = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(payload?.message || 'Request failed')
return payload
})
}
function StatCard({ label, value, icon }) {
return (
<div className="rounded-[24px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">{label}</div>
<div className="mt-3 flex items-center gap-3">
<span className="inline-flex h-12 w-12 items-center justify-center rounded-2xl border border-sky-300/20 bg-sky-400/10 text-sky-100">
<i className={`fa-solid ${icon}`} />
</span>
<span className="text-3xl font-semibold tracking-[-0.04em] text-white">{value}</span>
</div>
</div>
)
}
export default function StudioCardsIndex() {
const { props } = usePage()
const cards = props.cards?.data || []
const stats = props.stats || {}
const endpoints = props.endpoints || {}
async function duplicateCard(cardId) {
const url = (endpoints.duplicatePattern || '').replace('__CARD__', String(cardId))
if (!url) return
const payload = await requestJson(url)
if (payload?.data?.id) {
window.location.assign((endpoints.editPattern || '/studio/cards/__CARD__/edit').replace('__CARD__', String(payload.data.id)))
}
}
return (
<StudioLayout title="Nova Cards">
<Head title="Nova Cards Studio" />
<section className="rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.15),transparent_38%),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)]">
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl">
<p className="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75">Creation surface</p>
<h2 className="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white">Build quote cards, mood cards, and visual text art.</h2>
<p className="mt-3 text-sm leading-7 text-slate-300">Drafts autosave, templates stay structured, and every published card gets a public preview image ready for discovery and sharing.</p>
</div>
<div className="flex flex-wrap gap-3">
<Link href={endpoints.create || '/studio/cards/create'} className="inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">
<i className="fa-solid fa-plus" />
New card
</Link>
<a href="/cards" className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i className="fa-solid fa-compass" />
Browse public cards
</a>
</div>
</div>
</section>
<section className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatCard label="All cards" value={stats.all || 0} icon="fa-layer-group" />
<StatCard label="Drafts" value={stats.drafts || 0} icon="fa-file-lines" />
<StatCard label="Processing" value={stats.processing || 0} icon="fa-wand-magic-sparkles" />
<StatCard label="Published" value={stats.published || 0} icon="fa-earth-americas" />
</section>
<section className="mt-8">
<div className="mb-4 flex items-center justify-between gap-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Latest work</p>
<h3 className="mt-1 text-2xl font-semibold text-white">Your card library</h3>
</div>
</div>
{cards.length === 0 ? (
<div className="rounded-[28px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-16 text-center">
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-[24px] border border-white/12 bg-white/[0.05] text-slate-400">
<i className="fa-solid fa-rectangle-history-circle-user text-3xl" />
</div>
<h3 className="mt-5 text-2xl font-semibold text-white">No cards yet</h3>
<p className="mx-auto mt-3 max-w-xl text-sm leading-7 text-slate-300">Start with a square card or jump straight into a story-sized template. Your first draft will be created automatically in the editor.</p>
<Link href={endpoints.create || '/studio/cards/create'} className="mt-6 inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">
<i className="fa-solid fa-plus" />
Create your first card
</Link>
</div>
) : (
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{cards.map((card) => (
<div key={card.id} className="group rounded-[28px] border border-white/10 bg-white/[0.04] p-4 shadow-[0_22px_60px_rgba(2,6,23,0.22)] transition hover:-translate-y-1 hover:border-sky-300/30 hover:bg-white/[0.06]">
<a href={(endpoints.editPattern || '/studio/cards/__CARD__/edit').replace('__CARD__', String(card.id))}>
<NovaCardCanvasPreview card={card} className="w-full" />
<div className="mt-4 flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-lg font-semibold tracking-[-0.03em] text-white">{card.title}</div>
<div className="mt-1 line-clamp-2 text-sm leading-6 text-slate-300">{card.quote_text}</div>
</div>
<span className={`rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] ${card.status === 'published' ? 'border-emerald-300/25 bg-emerald-400/10 text-emerald-100' : card.status === 'processing' ? 'border-amber-300/25 bg-amber-400/10 text-amber-100' : 'border-white/10 bg-white/[0.05] text-slate-200'}`}>
{card.status}
</span>
</div>
<div className="mt-4 flex items-center justify-between text-xs text-slate-400">
<span>{card.category?.name || 'Uncategorized'}</span>
<span>{card.format}</span>
</div>
</a>
<div className="mt-4 flex gap-3">
<a href={(endpoints.editPattern || '/studio/cards/__CARD__/edit').replace('__CARD__', String(card.id))} className="flex-1 rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-center text-sm font-semibold text-white transition hover:bg-white/[0.08]">Edit</a>
<button type="button" onClick={() => duplicateCard(card.id)} className="rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">Duplicate</button>
</div>
</div>
))}
</div>
)}
</section>
</StudioLayout>
)
}