Implement academy analytics, billing, and web stories updates

This commit is contained in:
2026-05-26 07:27:29 +02:00
parent 456c3d6bb0
commit 0b33a1b074
177 changed files with 27360 additions and 2685 deletions

View File

@@ -0,0 +1,152 @@
import React from 'react'
import { Head, Link } from '@inertiajs/react'
import AccessBadge from '../../../components/academy/billing/AccessBadge'
function formatDate(iso) {
if (!iso) return null
try {
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
} catch {
return null
}
}
export default function AcademyBillingAccount({ currentTier, isSubscribed, subscription, activePlan = null, links = {} }) {
const endsAt = formatDate(subscription?.endsAt)
const onGracePeriod = subscription?.onGracePeriod === true
const subscriptionActive = subscription?.active === true
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.14),_transparent_26%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
<Head title="Academy Subscription" />
<div className="mx-auto max-w-[1280px] space-y-8">
{/* Header */}
<section className="rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.9),rgba(15,23,42,0.96))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10">
<div className="flex flex-wrap items-center gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100/85">Skinbase Academy</p>
<AccessBadge tier={currentTier} />
</div>
<h1 className="mt-4 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl">
{isSubscribed ? 'Your subscription' : 'Academy subscription'}
</h1>
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-300">
{isSubscribed
? 'Your Academy access is active. Manage, upgrade, or cancel your subscription here at any time.'
: 'You are on the free Academy tier. Upgrade to Creator or Pro to unlock premium content.'}
</p>
</section>
{/* Grace period warning */}
{onGracePeriod && endsAt ? (
<section className="rounded-[30px] border border-amber-300/25 bg-amber-300/[0.06] px-6 py-5">
<p className="font-semibold text-amber-100">Your subscription was cancelled and will end on {endsAt}.</p>
<p className="mt-2 text-sm leading-6 text-amber-100/75">You still have full access until that date. Open the subscription portal to resume your plan if you change your mind.</p>
<Link
href={links.portal}
className="mt-4 inline-flex items-center rounded-full border border-amber-300/30 bg-amber-300/12 px-5 py-2.5 text-sm font-semibold text-amber-100 transition hover:bg-amber-300/20"
>
Resume subscription
</Link>
</section>
) : null}
{/* No subscription: upgrade CTA */}
{!isSubscribed ? (
<section className="rounded-[30px] border border-white/10 bg-[linear-gradient(135deg,rgba(8,47,73,0.92),rgba(30,41,59,0.94))] p-6 md:p-7">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-100/85">Upgrade</p>
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.04em] text-white">Choose a plan to get started</h2>
<p className="mt-3 max-w-xl text-sm leading-7 text-slate-200/90">
Creator unlocks premium lessons and the full prompt library for 4.99/month. Pro gives you everything all lessons, the advanced content track, and every new Academy drop for 9.99/month.
</p>
<div className="mt-5 flex flex-wrap gap-3">
<Link
href={links.pricing || '/academy/pricing'}
className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18"
>
See plans and pricing
</Link>
<Link
href={links.academy || '/academy'}
className="rounded-full border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]"
>
Back to Academy
</Link>
</div>
</section>
) : null}
{/* Active subscription: details + manager */}
{isSubscribed ? (
<section className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_320px]">
<div className="space-y-5 rounded-[32px] border border-white/10 bg-white/[0.04] p-6 md:p-7">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Subscription details</p>
<div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Active plan</p>
<p className="mt-2 text-lg font-semibold text-white">{activePlan?.label || 'Academy plan'}</p>
{activePlan?.price_display ? (
<p className="mt-1 text-sm text-slate-400">{activePlan.price_display} / month</p>
) : null}
</div>
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Status</p>
<p className="mt-2 text-lg font-semibold capitalize text-white">
{onGracePeriod ? 'Cancelling' : subscriptionActive ? 'Active' : (subscription?.status || 'Active')}
</p>
{onGracePeriod && endsAt ? (
<p className="mt-1 text-sm text-amber-300/80">Access ends {endsAt}</p>
) : null}
{!onGracePeriod && subscriptionActive ? (
<p className="mt-1 text-sm text-emerald-300/80">Renews automatically</p>
) : null}
</div>
</div>
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Your Academy access</p>
<div className="mt-3 flex flex-wrap items-center gap-3">
<AccessBadge tier={currentTier} />
<p className="text-sm text-slate-300">
{currentTier === 'pro'
? 'Full access to all Academy lessons and content.'
: currentTier === 'creator'
? 'Full access to all Creator lessons and prompts.'
: 'Access to free Academy content.'}
</p>
</div>
</div>
</div>
<aside className="space-y-3 rounded-[32px] border border-white/10 bg-black/20 p-6">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300">Manage</p>
<p className="text-xs leading-6 text-slate-400">
Use the subscription portal to upgrade, downgrade, or cancel. Changes take effect at your next billing date.
</p>
<Link
href={links.portal}
className="mt-2 inline-flex w-full items-center justify-center rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18"
>
Upgrade, downgrade or cancel
</Link>
<Link
href={links.pricing || '/academy/pricing'}
className="inline-flex w-full items-center justify-center rounded-full border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]"
>
Compare plans
</Link>
<Link
href={links.academy || '/academy'}
className="inline-flex w-full items-center justify-center rounded-full border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]"
>
Go to Academy
</Link>
</aside>
</section>
) : null}
</div>
</main>
)
}

View File

@@ -0,0 +1,23 @@
import React from 'react'
import { Head, Link } from '@inertiajs/react'
export default function AcademyBillingCancel({ message, links = {} }) {
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(148,163,184,0.14),_transparent_26%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
<Head title="Academy Billing Canceled" />
<div className="mx-auto max-w-[920px] space-y-8">
<section className="rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.9),rgba(67,20,7,0.78))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-100/85">Checkout canceled</p>
<h1 className="mt-4 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl">No payment was made.</h1>
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-300">{message}</p>
</section>
<div className="flex flex-wrap gap-3">
<Link href={links.pricing || '/academy/pricing'} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18">Return to pricing</Link>
<Link href={links.academy || '/academy'} className="rounded-full border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]">Back to Academy</Link>
</div>
</div>
</main>
)
}

View File

@@ -0,0 +1,220 @@
import React from 'react'
import { usePage, Link } from '@inertiajs/react'
import SeoHead from '../../../components/seo/SeoHead'
import AccessBadge from '../../../components/academy/billing/AccessBadge'
import PlanCard from '../../../components/academy/billing/PlanCard'
import { trackUpgradeClick, useAcademyPageAnalytics } from '../../../lib/academyAnalytics'
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
function heroText(currentTier, isSubscribed) {
if (isSubscribed && currentTier === 'pro') {
return {
heading: 'You have full Academy access.',
body: 'All lessons, prompts, and Academy content are unlocked on your Pro plan. To upgrade, downgrade, or cancel, use the subscription manager below.',
}
}
if (isSubscribed && currentTier === 'creator') {
return {
heading: "You're on the Creator plan.",
body: 'Creator content is fully unlocked. Upgrade to Pro anytime to access the advanced lesson track and everything new that launches at the Pro tier.',
}
}
if (currentTier === 'admin') {
return {
heading: 'Academy plans.',
body: 'Your admin account already has full Academy access. Browse the plans below.',
}
}
if (isSubscribed) {
return {
heading: 'Manage your Academy subscription.',
body: 'Your plan is active. Review your options below or use the subscription manager to make changes.',
}
}
return {
heading: 'Unlock everything in Academy.',
body: "Start free and upgrade when you're ready. Creator unlocks premium lessons and the full prompt library. Pro adds the advanced lesson track and is the highest Academy tier.",
}
}
function SidePanel({ currentTier, isSubscribed, activePlanLabel, activePlanPrice, manageHref }) {
if (isSubscribed) {
return (
<div className="rounded-[30px] border border-emerald-300/20 bg-emerald-300/[0.06] p-6">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-200/80">Your subscription</p>
<div className="mt-4 space-y-3">
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
<p className="text-[10px] uppercase tracking-[0.16em] text-slate-500">Active plan</p>
<p className="mt-1 text-sm font-semibold text-white">{activePlanLabel || 'Academy plan'}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
<p className="text-[10px] uppercase tracking-[0.16em] text-slate-500">Billed monthly</p>
<p className="mt-1 text-sm font-semibold text-white">{activePlanPrice || '—'}</p>
</div>
</div>
{manageHref ? (
<Link href={manageHref} className="mt-5 inline-flex w-full items-center justify-center rounded-full border border-emerald-300/30 bg-emerald-300/10 px-5 py-3 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-300/18">
Manage subscription
</Link>
) : null}
</div>
)
}
return (
<div className="rounded-[30px] border border-white/10 bg-black/20 p-6">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Why upgrade?</p>
<div className="mt-4 space-y-3">
{[
{ title: 'Instant access', body: 'Subscription activates the moment payment is confirmed.' },
{ title: 'Cancel anytime', body: 'No lock-in. Keep access until the end of the billing period.' },
{ title: 'Switch freely', body: 'Move between Creator and Pro from your subscription manager.' },
].map(({ title, body }) => (
<div key={title} className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3">
<p className="text-sm font-semibold text-white">{title}</p>
<p className="mt-1 text-xs leading-5 text-slate-400">{body}</p>
</div>
))}
</div>
</div>
)
}
export default function AcademyBillingPricing({ seo, billingEnabled, currentTier, isSubscribed, activePlanKey = null, activePlanLabel = null, catalog = [], links = {}, analytics }) {
const { auth, errors, flash } = usePage().props
useAcademyPageAnalytics(analytics)
const loginHref = auth?.user ? null : `${links.login || '/login'}?intended=${encodeURIComponent(links.pricing || '/academy/pricing')}`
const products = catalog.map((product) => ({
...product,
selectedPlan: product.plans[0] || null,
}))
const activePlanPrice = products
.flatMap((p) => p.plans)
.find((p) => p?.key === activePlanKey)?.price_display || null
const handleCheckout = (plan) => {
if (!plan?.key || !links.checkout) return
trackUpgradeClick(analytics, {
source: 'academy_billing_pricing',
academy_plan: plan.key,
academy_interval: plan.interval,
})
const form = document.createElement('form')
form.method = 'POST'
form.action = links.checkout
form.style.display = 'none'
const csrfInput = document.createElement('input')
csrfInput.type = 'hidden'
csrfInput.name = '_token'
csrfInput.value = getCsrfToken()
const planInput = document.createElement('input')
planInput.type = 'hidden'
planInput.name = 'plan'
planInput.value = plan.key
form.appendChild(csrfInput)
form.appendChild(planInput)
document.body.appendChild(form)
form.submit()
}
const hero = heroText(currentTier, isSubscribed)
const showFreeBadgeAsCurrentPlan = currentTier === 'free' && !isSubscribed && auth?.user
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.16),_transparent_22%),radial-gradient(circle_at_bottom_right,_rgba(56,189,248,0.18),_transparent_26%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
<SeoHead seo={seo || {}} title="Skinbase Academy — Plans & Pricing" description={seo?.description} />
<div className="mx-auto max-w-[1380px] space-y-8">
{/* Hero */}
<section className="overflow-hidden rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.9),rgba(67,20,7,0.82))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10 lg:p-12">
<div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_320px] xl:items-start">
<div>
<div className="flex flex-wrap items-center gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-200/85">Skinbase Academy</p>
{currentTier !== 'free' ? <AccessBadge tier={currentTier} /> : null}
</div>
<h1 className="mt-4 max-w-3xl text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl xl:text-6xl">{hero.heading}</h1>
<p className="mt-5 max-w-2xl text-base leading-8 text-slate-300 md:text-lg">{hero.body}</p>
{errors?.plan ? <p className="mt-4 text-sm font-medium text-rose-200">{errors.plan}</p> : null}
{flash?.error ? <p className="mt-4 rounded-2xl border border-rose-300/20 bg-rose-300/10 px-4 py-3 text-sm font-medium text-rose-100">{flash.error}</p> : null}
{flash?.success ? <p className="mt-4 rounded-2xl border border-emerald-300/20 bg-emerald-300/10 px-4 py-3 text-sm font-medium text-emerald-100">{flash.success}</p> : null}
</div>
<SidePanel
currentTier={currentTier}
isSubscribed={isSubscribed}
activePlanLabel={activePlanLabel}
activePlanPrice={activePlanPrice}
manageHref={links.billingAccount}
/>
</div>
</section>
{/* Plan grid */}
<section className="grid gap-5 xl:grid-cols-[minmax(0,0.9fr)_1fr_1fr]">
{/* Free / Explorer card */}
<article className={`rounded-[32px] border p-6 md:p-7 ${showFreeBadgeAsCurrentPlan ? 'border-white/20 bg-white/[0.06]' : 'border-white/10 bg-white/[0.04]'}`}>
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Free</p>
<h2 className="mt-3 text-3xl font-semibold tracking-[-0.05em] text-white">Explorer</h2>
</div>
{showFreeBadgeAsCurrentPlan
? <span className="rounded-full border border-white/20 bg-white/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-200">Your plan</span>
: <AccessBadge tier="free" />}
</div>
<div className="mt-6 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4">
<p className="text-3xl font-semibold tracking-[-0.04em] text-white">Free</p>
<p className="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">No payment needed</p>
</div>
<p className="mt-4 text-sm leading-7 text-slate-300">Everything you need to explore Academy, follow public lessons, and see a preview of what the paid tiers include.</p>
<div className="mt-5 space-y-3 text-sm text-slate-300">
{[
'Public lessons and Academy listings',
'Prompt previews and public documentation',
'Community access and updates',
'Upgrade to Creator or Pro anytime',
].map((feature) => (
<div key={feature} className="flex items-start gap-2.5 rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
<span className="mt-px shrink-0 text-slate-500"></span>
<span>{feature}</span>
</div>
))}
</div>
</article>
{products.map((product) => (
<PlanCard
key={product.tier}
product={product}
selectedPlan={product.selectedPlan}
currentTier={currentTier}
isSubscribed={isSubscribed}
activePlanKey={activePlanKey}
billingEnabled={billingEnabled}
loginHref={loginHref}
manageHref={links.billingAccount}
onCheckout={handleCheckout}
/>
))}
</section>
</div>
</main>
)
}

View File

@@ -0,0 +1,45 @@
import React from 'react'
import { Head, Link } from '@inertiajs/react'
import AccessBadge from '../../../components/academy/billing/AccessBadge'
export default function AcademyBillingSuccess({ currentTier, isSubscribed, links = {} }) {
return (
<main className="flex min-h-screen items-center bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(16,185,129,0.14),_transparent_24%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
<Head title="Subscription Confirmed" />
<div className="mx-auto w-full max-w-[640px] space-y-6">
<section className="rounded-[40px] border border-emerald-300/20 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.92),rgba(6,78,59,0.82))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10">
<div className="flex items-center gap-3">
<span className="text-3xl leading-none">🎉</span>
{isSubscribed ? <AccessBadge tier={currentTier} /> : null}
</div>
<h1 className="mt-5 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl">
{isSubscribed ? 'Welcome to Academy.' : "You're all set."}
</h1>
<p className="mt-4 max-w-lg text-base leading-8 text-slate-300">
{isSubscribed
? 'Your subscription is active and all premium content for your plan is now unlocked. Head to Academy and start exploring.'
: "Your payment was confirmed and your subscription is activating now. This usually takes just a moment. If you don't see your access right away, refresh the Academy page in a few seconds."}
</p>
</section>
<div className="flex flex-wrap gap-3">
<Link
href={links.academy || '/academy'}
className="rounded-full border border-emerald-300/25 bg-emerald-300/12 px-5 py-3 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-300/18"
>
Go to Academy
</Link>
{links.account ? (
<Link
href={links.account}
className="rounded-full border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]"
>
View my subscription
</Link>
) : null}
</div>
</div>
</main>
)
}