Optimize academy
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Head, Link } from '@inertiajs/react'
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { Head, Link, useForm, usePage } from '@inertiajs/react'
|
||||
import AccessBadge from '../../../components/academy/billing/AccessBadge'
|
||||
|
||||
function formatDate(iso) {
|
||||
@@ -12,6 +12,72 @@ function formatDate(iso) {
|
||||
}
|
||||
|
||||
export default function AcademyBillingAccount({ currentTier, isSubscribed, subscription, activePlan = null, links = {} }) {
|
||||
const { flash, auth } = usePage().props
|
||||
const { data, setData, post, processing } = useForm({
|
||||
issue_type: 'billing',
|
||||
contact_email: auth?.user?.email || '',
|
||||
message: '',
|
||||
session_id: null,
|
||||
})
|
||||
|
||||
function IssueTypeDropdown({ value, onChange }) {
|
||||
const options = [
|
||||
{ value: 'billing', label: 'Billing question' },
|
||||
{ value: 'payment', label: 'Payment problem' },
|
||||
{ value: 'upgrade', label: 'Upgrade problem' },
|
||||
{ value: 'downgrade', label: 'Downgrade problem' },
|
||||
{ value: 'cancel', label: 'Cancellation problem' },
|
||||
{ value: 'access', label: 'Access not updated' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
]
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
function onDoc(e) {
|
||||
if (ref.current && !ref.current.contains(e.target)) setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', onDoc)
|
||||
return () => document.removeEventListener('mousedown', onDoc)
|
||||
}, [])
|
||||
|
||||
const current = options.find((o) => o.value === value) || options[0]
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((s) => !s)}
|
||||
className="w-full text-left rounded-xl border border-amber-300/20 bg-black/20 p-3 text-sm text-amber-50 flex items-center justify-between"
|
||||
>
|
||||
<span>{current.label}</span>
|
||||
<svg className="ml-2 h-4 w-4 text-amber-100/70" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 8l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<div className="absolute left-0 top-full mt-2 w-full rounded-xl border border-white/10 bg-[#10192e] shadow-2xl z-50 overflow-hidden">
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(opt.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
className={`w-full text-left px-4 py-3 text-sm ${opt.value === value ? 'bg-white/[0.03] text-white' : 'text-slate-300 hover:bg-white/[0.02]'}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
const endsAt = formatDate(subscription?.endsAt)
|
||||
const onGracePeriod = subscription?.onGracePeriod === true
|
||||
const subscriptionActive = subscription?.active === true
|
||||
@@ -21,6 +87,16 @@ export default function AcademyBillingAccount({ currentTier, isSubscribed, subsc
|
||||
<Head title="Academy Subscription" />
|
||||
|
||||
<div className="mx-auto max-w-[1280px] space-y-8">
|
||||
{flash?.error ? (
|
||||
<section className="rounded-[20px] border border-rose-300/20 bg-rose-300/8 p-4">
|
||||
<p className="font-semibold text-rose-100">{flash.error}</p>
|
||||
</section>
|
||||
) : null}
|
||||
{flash?.success ? (
|
||||
<section className="rounded-[20px] border border-emerald-300/20 bg-emerald-300/8 p-4">
|
||||
<p className="font-semibold text-emerald-100">{flash.success}</p>
|
||||
</section>
|
||||
) : null}
|
||||
{/* 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">
|
||||
@@ -42,12 +118,12 @@ export default function AcademyBillingAccount({ currentTier, isSubscribed, subsc
|
||||
<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
|
||||
<a
|
||||
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>
|
||||
</a>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
@@ -123,20 +199,75 @@ export default function AcademyBillingAccount({ currentTier, isSubscribed, subsc
|
||||
<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.
|
||||
Use the subscription portal to cancel or manage billing details. Plan upgrades are handled here on Skinbase.
|
||||
</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>
|
||||
{/* Use a plain anchor to perform a full navigation to Stripe (avoid Inertia XHR/CORS) */}
|
||||
<a
|
||||
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"
|
||||
>
|
||||
Open billing portal
|
||||
</a>
|
||||
<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>
|
||||
{/* Quick upgrade form: allow Creator -> Pro upgrade in one click (full POST, not Inertia) */}
|
||||
{activePlan?.tier === 'creator' ? (
|
||||
<form action={links.checkout} method="POST" data-no-inertia className="mt-2">
|
||||
<input type="hidden" name="_token" value={getCsrfToken()} />
|
||||
<input type="hidden" name="plan" value="pro_monthly" />
|
||||
<button type="submit" className="inline-flex w-full items-center justify-center rounded-full border border-emerald-300/25 bg-emerald-300/10 px-5 py-3 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-300/18">Upgrade to Pro now</button>
|
||||
</form>
|
||||
) : null}
|
||||
{links.reportIssue ? (
|
||||
<div className="mt-3 rounded-2xl border border-amber-300/20 bg-amber-300/8 p-4">
|
||||
<p className="text-sm font-semibold text-amber-100">Need help with billing or access?</p>
|
||||
<p className="mt-1 text-xs leading-5 text-amber-100/80">
|
||||
Send a quick report here if payment, access, or subscription changes do not behave as expected.
|
||||
</p>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
post(links.reportIssue, { preserveScroll: true })
|
||||
}}
|
||||
className="mt-3 space-y-3"
|
||||
>
|
||||
<div className="grid gap-3">
|
||||
<label className="space-y-1 relative">
|
||||
<span className="text-xs font-medium text-amber-100/80">Issue type</span>
|
||||
{/* Custom dropdown to avoid native browser option styling */}
|
||||
<IssueTypeDropdown value={data.issue_type} onChange={(v) => setData('issue_type', v)} />
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs font-medium text-amber-100/80">Reply email</span>
|
||||
<input
|
||||
type="email"
|
||||
value={data.contact_email}
|
||||
onChange={(event) => setData('contact_email', event.target.value)}
|
||||
placeholder="you@example.com"
|
||||
className="w-full rounded-xl border border-amber-300/20 bg-black/20 p-3 text-sm text-amber-50 placeholder:text-amber-100/40"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
value={data.message}
|
||||
onChange={(event) => setData('message', event.target.value)}
|
||||
placeholder="Describe the issue you hit, what you expected, and anything already charged or missing"
|
||||
className="min-h-[96px] w-full rounded-xl border border-amber-300/20 bg-black/20 p-3 text-sm text-amber-50 placeholder:text-amber-100/40"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="inline-flex w-full items-center justify-center rounded-full border border-amber-300/30 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100 transition hover:bg-amber-300/18 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{processing ? 'Sending report...' : 'Send support report'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
<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]"
|
||||
|
||||
@@ -83,7 +83,7 @@ function SidePanel({ currentTier, isSubscribed, activePlanLabel, activePlanPrice
|
||||
)
|
||||
}
|
||||
|
||||
export default function AcademyBillingPricing({ seo, billingEnabled, currentTier, isSubscribed, activePlanKey = null, activePlanLabel = null, catalog = [], links = {}, analytics }) {
|
||||
export default function AcademyBillingPricing({ seo, billingEnabled, currentTier, isSubscribed, activePlanKey = null, activePlanLabel = null, catalog = [], links = {}, analytics, missingRemote = [] }) {
|
||||
const { auth, errors, flash } = usePage().props
|
||||
|
||||
useAcademyPageAnalytics(analytics)
|
||||
@@ -151,6 +151,12 @@ export default function AcademyBillingPricing({ seo, billingEnabled, currentTier
|
||||
{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}
|
||||
{Array.isArray(missingRemote) && missingRemote.length > 0 ? (
|
||||
<div className="mt-4 rounded-2xl border border-amber-300/20 bg-amber-300/8 px-4 py-3 text-sm font-medium text-amber-50">
|
||||
<p className="font-semibold">Purchases temporarily disabled:</p>
|
||||
<p className="mt-1 text-xs">The following plans could not be verified in Stripe: {missingRemote.join(', ')}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<SidePanel
|
||||
@@ -217,4 +223,4 @@ export default function AcademyBillingPricing({ seo, billingEnabled, currentTier
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react'
|
||||
import { Head, Link } from '@inertiajs/react'
|
||||
import { Head, Link, usePage, useForm } from '@inertiajs/react'
|
||||
import AccessBadge from '../../../components/academy/billing/AccessBadge'
|
||||
|
||||
export default function AcademyBillingSuccess({ currentTier, isSubscribed, links = {} }) {
|
||||
const { auth } = usePage().props
|
||||
const sessionId = usePage().props.sessionId || null
|
||||
const userEmail = auth?.user?.email ?? null
|
||||
const { data, setData, post, processing } = useForm({ message: '', session_id: sessionId })
|
||||
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" />
|
||||
@@ -21,6 +25,25 @@ export default function AcademyBillingSuccess({ currentTier, isSubscribed, links
|
||||
? '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>
|
||||
|
||||
{!isSubscribed ? (
|
||||
<div className="mt-4 rounded-2xl border border-amber-300/20 bg-amber-300/8 px-4 py-3 text-sm text-amber-50">
|
||||
<p className="font-semibold">If your access isn't updated automatically</p>
|
||||
<p className="mt-1">If your Academy access doesn't appear within a few minutes, email <strong>academy@skinbase.org</strong> or click the button below to open a prefilled message. Include your account email{userEmail ? ` (${userEmail})` : ''} and the checkout session id{sessionId ? `: ${sessionId}` : '.'}</p>
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-start gap-4">
|
||||
<form onSubmit={(e) => { e.preventDefault(); post(links.reportIssue, { preserveScroll: true }) }} className="flex w-full max-w-lg items-start gap-2">
|
||||
<textarea value={data.message} onChange={(e) => setData('message', e.target.value)} placeholder="Optional: Tell us what you expected to see or any useful details" className="flex-1 rounded-md bg-black/20 border border-amber-300/20 p-2 text-sm text-amber-50" rows={3} />
|
||||
<button type="submit" disabled={processing} className="rounded-full border border-amber-300/30 bg-amber-300/12 px-4 py-2 text-sm font-semibold text-amber-900 hover:bg-amber-300/16">Send report</button>
|
||||
</form>
|
||||
|
||||
<div className="text-xs text-amber-100">
|
||||
<div>- Wait 2–3 minutes and refresh the Academy page.</div>
|
||||
<div>- If you still lack access, use the form above or email academy@skinbase.org.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
|
||||
Reference in New Issue
Block a user