Files
SkinbaseNova/resources/js/Pages/Admin/Academy/AnalyticsIntelligence.jsx

307 lines
16 KiB
JavaScript

import React, { useState } from 'react'
import { Head, router } from '@inertiajs/react'
import AdminLayout from '../../../Layouts/AdminLayout'
import AnalyticsNav from './AnalyticsNav'
function SummaryCard({ label, value, description }) {
return (
<div className="rounded-[28px] border border-white/[0.08] bg-white/[0.04] p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500">{label}</p>
<p className="mt-3 text-3xl font-bold text-white">{Number(value || 0).toLocaleString()}</p>
<p className="mt-3 text-sm leading-6 text-slate-300">{description}</p>
</div>
)
}
function RangeControls({ range }) {
const pathname = typeof window !== 'undefined' ? window.location.pathname : ''
const [from, setFrom] = useState(range?.from || '')
const [to, setTo] = useState(range?.to || '')
const visit = (nextRange, nextFrom = from, nextTo = to) => {
router.get(pathname, {
range: nextRange,
...(nextRange === 'custom' ? { from: nextFrom, to: nextTo } : {}),
}, {
preserveScroll: true,
preserveState: true,
replace: true,
})
}
return (
<div className="rounded-[28px] border border-white/[0.08] bg-white/[0.03] p-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500">Date Range</p>
<p className="mt-3 text-sm text-slate-300">{range?.from} to {range?.to}</p>
</div>
<div className="flex flex-wrap gap-2">
{(range?.options || []).map((option) => {
const active = option.value === range?.active
return (
<button
key={option.value}
type="button"
onClick={() => visit(option.value)}
className={`rounded-full border px-4 py-2 text-sm font-semibold transition ${active ? 'border-sky-300/30 bg-sky-300/12 text-sky-100' : 'border-white/[0.08] bg-white/[0.04] text-slate-300 hover:border-white/15 hover:bg-white/[0.06] hover:text-white'}`}
>
{option.label}
</button>
)
})}
</div>
</div>
<div className="mt-5 flex flex-wrap items-end gap-3 border-t border-white/[0.08] pt-5">
<label className="flex flex-col gap-2 text-sm text-slate-300">
<span>From</span>
<input type="date" value={from} onChange={(event) => setFrom(event.target.value)} className="rounded-2xl border border-white/[0.08] bg-black/20 px-4 py-3 text-white outline-none transition focus:border-sky-300/30" />
</label>
<label className="flex flex-col gap-2 text-sm text-slate-300">
<span>To</span>
<input type="date" value={to} onChange={(event) => setTo(event.target.value)} className="rounded-2xl border border-white/[0.08] bg-black/20 px-4 py-3 text-white outline-none transition focus:border-sky-300/30" />
</label>
<button type="button" onClick={() => visit('custom', from, to)} className="rounded-2xl border border-white/[0.08] bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/15 hover:bg-white/[0.06]">
Apply Custom Range
</button>
</div>
</div>
)
}
function Section({ title, description, children }) {
return (
<section className="rounded-[30px] border border-white/[0.08] bg-white/[0.03] p-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500">{title}</p>
<p className="mt-3 max-w-3xl text-sm leading-6 text-slate-300">{description}</p>
</div>
</div>
<div className="mt-5">{children}</div>
</section>
)
}
function EmptyState({ text }) {
return <div className="rounded-2xl border border-dashed border-white/[0.08] bg-black/20 px-4 py-8 text-sm text-slate-400">{text}</div>
}
function Badge({ children, tone = 'default' }) {
const tones = {
default: 'border-white/[0.08] bg-white/[0.04] text-slate-200',
high: 'border-rose-300/25 bg-rose-300/10 text-rose-100',
medium: 'border-amber-300/25 bg-amber-300/10 text-amber-100',
low: 'border-emerald-300/25 bg-emerald-300/10 text-emerald-100',
}
return <span className={`inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] ${tones[tone] || tones.default}`}>{children}</span>
}
function Table({ columns, children }) {
return (
<div className="overflow-x-auto rounded-[24px] border border-white/[0.08] bg-black/20">
<table className="min-w-full divide-y divide-white/[0.08] text-left">
<thead>
<tr>
{columns.map((column) => (
<th key={column} className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{column}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-white/[0.06]">{children}</tbody>
</table>
</div>
)
}
function OpportunityHighlights({ items = [] }) {
if (!items.length) {
return <EmptyState text="Recommendations will appear here once Academy analytics has enough activity in the selected range." />
}
return (
<div className="grid gap-4 xl:grid-cols-2">
{items.map((item, index) => (
<div key={`${item.title}-${index}`} className="rounded-[24px] border border-white/[0.08] bg-black/20 p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-base font-semibold text-white">{item.title}</p>
<Badge tone={item.priority}>{item.priority}</Badge>
</div>
<p className="mt-3 text-sm leading-6 text-slate-300">{item.reason}</p>
<p className="mt-4 text-sm font-semibold text-sky-100">{item.suggested_action}</p>
</div>
))}
</div>
)
}
export default function AcademyAnalyticsIntelligence({
nav = [],
range,
contentOpportunities = {},
searchGaps = {},
promptInsights = {},
lessonDropoffs = {},
courseHealth = {},
premiumInterest = {},
editorialRecommendations = {},
}) {
return (
<AdminLayout title="Academy Content Intelligence" subtitle="Editorial and business signals from Academy rollups, search demand, engagement, and premium intent.">
<Head title="Admin · Academy Content Intelligence" />
<div className="space-y-6">
<AnalyticsNav items={nav} />
<RangeControls range={range} />
<Section title="Content Opportunities" description="A fast view of where Academy demand is strongest, where content is underperforming, and which changes should be prioritized next.">
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-7">
{(contentOpportunities?.cards || []).map((card) => (
<SummaryCard key={card.label} label={card.label} value={card.value} description={card.description} />
))}
</div>
<div className="mt-6">
<OpportunityHighlights items={contentOpportunities?.highlights || []} />
</div>
</Section>
<Section title="Search Gaps" description="Queries that suggest missing content, weak relevance, or topics worth expanding because users are clearly engaging with them.">
{searchGaps?.rows?.length ? (
<Table columns={['Query', 'Searches', 'Results', 'Clicks', 'CTR', 'Suggested Action']}>
{searchGaps.rows.map((row) => (
<tr key={row.normalized_query}>
<td className="px-4 py-4 align-top">
<p className="font-semibold text-white">{row.query}</p>
<div className="mt-2 flex flex-wrap gap-2">
<Badge tone={row.priority}>{row.issue}</Badge>
{row.logged_in_searches > 1 ? <Badge>Logged-in x{row.logged_in_searches}</Badge> : null}
{row.subscriber_searches > 0 ? <Badge>Subscribers x{row.subscriber_searches}</Badge> : null}
</div>
</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.searches}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.results_count}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.clicks}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.ctr}%</td>
<td className="px-4 py-4 text-sm leading-6 text-slate-300">{row.suggested_action}</td>
</tr>
))}
</Table>
) : <EmptyState text="No Academy search gaps were detected in this range." />}
</Section>
<Section title="Prompt Insights" description="Signals that show whether prompts need better quality, stronger discoverability, more examples, or a premium follow-up.">
{promptInsights?.rows?.length ? (
<Table columns={['Prompt', 'Views', 'Copies', 'Copy Rate', 'Saves', 'Likes', 'Issue', 'Suggested Action']}>
{promptInsights.rows.map((row) => (
<tr key={row.content_id}>
<td className="px-4 py-4 align-top">
<p className="font-semibold text-white">{row.title}</p>
<p className="mt-2 text-xs uppercase tracking-[0.18em] text-slate-500">{row.content_type_label}</p>
</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.views}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.prompt_copies}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.copy_rate}%</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.saves}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.likes}</td>
<td className="px-4 py-4"><Badge tone={row.priority}>{row.issue}</Badge></td>
<td className="px-4 py-4 text-sm leading-6 text-slate-300">{row.suggested_action}</td>
</tr>
))}
</Table>
) : <EmptyState text="No prompt intelligence signals were detected in this range." />}
</Section>
<Section title="Lesson Drop-offs" description="Lessons where users hesitate to start, fail to finish, or unexpectedly show strong premium interest.">
{lessonDropoffs?.rows?.length ? (
<Table columns={['Lesson', 'Views', 'Starts', 'Completions', 'Completion Rate', 'Issue', 'Suggested Action']}>
{lessonDropoffs.rows.map((row) => (
<tr key={row.content_id}>
<td className="px-4 py-4 align-top">
<p className="font-semibold text-white">{row.title}</p>
<p className="mt-2 text-xs uppercase tracking-[0.18em] text-slate-500">Start rate {row.start_rate}%</p>
</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.views}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.starts}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.completions}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.completion_rate}%</td>
<td className="px-4 py-4"><Badge tone={row.priority}>{row.issue}</Badge></td>
<td className="px-4 py-4 text-sm leading-6 text-slate-300">{row.suggested_action}</td>
</tr>
))}
</Table>
) : <EmptyState text="No lesson drop-off signals were detected in this range." />}
</Section>
<Section title="Course Health" description="Courses that need better positioning or restructuring, plus courses that have enough momentum to justify expansion.">
{courseHealth?.rows?.length ? (
<Table columns={['Course', 'Views', 'Starts', 'Completions', 'Completion Rate', 'Avg Progress', 'Suggested Action']}>
{courseHealth.rows.map((row) => (
<tr key={row.content_id}>
<td className="px-4 py-4 align-top">
<p className="font-semibold text-white">{row.title}</p>
<div className="mt-2 flex flex-wrap gap-2">
<Badge tone={row.priority}>{row.issue}</Badge>
{row.learners > 0 ? <Badge>Learners {row.learners}</Badge> : null}
</div>
</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.views}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.starts}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.completions}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.completion_rate}%</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.avg_progress}%</td>
<td className="px-4 py-4 text-sm leading-6 text-slate-300">{row.suggested_action}</td>
</tr>
))}
</Table>
) : <EmptyState text="No course health signals were detected in this range." />}
</Section>
<Section title="Premium Interest" description="Free and premium Academy content that either converts well into upgrade intent or needs stronger teaser positioning.">
{premiumInterest?.rows?.length ? (
<Table columns={['Content', 'Type', 'Premium Views', 'Upgrade Clicks', 'Upgrade Rate', 'Suggested Action']}>
{premiumInterest.rows.map((row) => (
<tr key={`${row.content_type}-${row.content_id}`}>
<td className="px-4 py-4 align-top">
<p className="font-semibold text-white">{row.title}</p>
<div className="mt-2 flex flex-wrap gap-2">
<Badge tone={row.priority}>{row.issue}</Badge>
<Badge>Interest score {row.premium_interest_score}</Badge>
</div>
</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.content_type_label}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.premium_preview_views}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.upgrade_clicks}</td>
<td className="px-4 py-4 text-sm text-slate-200">{row.upgrade_rate}%</td>
<td className="px-4 py-4 text-sm leading-6 text-slate-300">{row.suggested_action}</td>
</tr>
))}
</Table>
) : <EmptyState text="No premium interest signals were detected in this range." />}
</Section>
<Section title="Editorial Recommendations" description="Prioritized recommendations that combine content demand, user behavior, and premium intent into concrete next actions.">
{editorialRecommendations?.rows?.length ? (
<div className="grid gap-4 xl:grid-cols-2">
{editorialRecommendations.rows.map((row, index) => (
<div key={`${row.title}-${index}`} className="rounded-[24px] border border-white/[0.08] bg-black/20 p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-base font-semibold text-white">{row.title}</p>
<Badge tone={row.priority}>{row.priority}</Badge>
</div>
<p className="mt-3 text-sm leading-6 text-slate-300">{row.description}</p>
<p className="mt-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Reason</p>
<p className="mt-2 text-sm leading-6 text-slate-300">{row.reason}</p>
<p className="mt-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Suggested Action</p>
<p className="mt-2 text-sm font-semibold leading-6 text-sky-100">{row.suggested_action}</p>
</div>
))}
</div>
) : <EmptyState text="No editorial recommendations were generated for this range yet." />}
</Section>
</div>
</AdminLayout>
)
}