optimizations
This commit is contained in:
File diff suppressed because it is too large
Load Diff
84
resources/js/Pages/Studio/StudioCardAnalytics.jsx
Normal file
84
resources/js/Pages/Studio/StudioCardAnalytics.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1500
resources/js/Pages/Studio/StudioCardEditor.jsx
Normal file
1500
resources/js/Pages/Studio/StudioCardEditor.jsx
Normal file
File diff suppressed because it is too large
Load Diff
134
resources/js/Pages/Studio/StudioCardsIndex.jsx
Normal file
134
resources/js/Pages/Studio/StudioCardsIndex.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user