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

@@ -74,13 +74,27 @@ function searchResultContentType(pageType) {
return null
}
function promptPreviewAsset(item) {
const full = item?.preview_image || ''
const thumb = item?.preview_image_thumb || full
if (!thumb) {
return null
}
return {
src: thumb,
srcSet: item?.preview_image_srcset || '',
}
}
function PromptLibraryHero({ title, description, items, pricingUrl, totalCount }) {
const featuredImages = (items || [])
.map((item) => item?.preview_image)
.map((item) => promptPreviewAsset(item))
.filter(Boolean)
.slice(0, 3)
const primaryImage = featuredImages[0] || ''
const primaryImage = featuredImages[0] || null
const supportingImages = featuredImages.slice(1, 3)
return (
@@ -119,14 +133,14 @@ function PromptLibraryHero({ title, description, items, pricingUrl, totalCount }
{primaryImage ? (
<>
<div className="overflow-hidden rounded-[28px] border border-white/10 bg-black/20 shadow-[0_18px_45px_rgba(2,6,23,0.18)] aspect-[16/10]">
<img src={primaryImage} alt="" aria-hidden="true" className="h-full w-full object-cover" />
<img src={primaryImage.src} srcSet={primaryImage.srcSet || undefined} sizes="(max-width: 1279px) calc(100vw - 4rem), 420px" alt="" aria-hidden="true" className="h-full w-full object-cover" />
</div>
{supportingImages.length ? (
<div className={`grid gap-3 ${supportingImages.length === 1 ? 'grid-cols-1' : 'grid-cols-2'}`}>
{supportingImages.map((image, index) => (
<div key={`${image}-${index}`} className="overflow-hidden rounded-[28px] border border-white/10 bg-black/20 shadow-[0_18px_45px_rgba(2,6,23,0.18)] aspect-square">
<img src={image} alt="" aria-hidden="true" className="h-full w-full object-cover" />
<div key={`${image.src}-${index}`} className="overflow-hidden rounded-[28px] border border-white/10 bg-black/20 shadow-[0_18px_45px_rgba(2,6,23,0.18)] aspect-square">
<img src={image.src} srcSet={image.srcSet || undefined} sizes="(max-width: 1279px) calc(50vw - 2rem), 200px" alt="" aria-hidden="true" className="h-full w-full object-cover" />
</div>
))}
</div>
@@ -145,7 +159,8 @@ function PromptLibraryHero({ title, description, items, pricingUrl, totalCount }
function AcademyCard({ pageType, item, analytics, searchContext, position }) {
const lessonSeries = String(item?.series_name || '').trim()
const promptPreviewImage = item?.preview_image || ''
const promptPreviewImage = item?.preview_image_thumb || item?.preview_image || ''
const promptPreviewSrcSet = item?.preview_image_srcset || ''
const contentType = searchResultContentType(pageType)
const href = itemHref(pageType, item)
const trackSearchClick = () => {
@@ -173,7 +188,7 @@ function AcademyCard({ pageType, item, analytics, searchContext, position }) {
className="group overflow-hidden rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(7,11,18,0.96))] shadow-[0_20px_50px_rgba(2,6,23,0.18)] transition hover:border-sky-300/25 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]"
>
<div className="relative aspect-[16/11] overflow-hidden bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(17,24,39,0.94))]">
{promptPreviewImage ? <img src={promptPreviewImage} alt="" aria-hidden="true" className="h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" /> : null}
{promptPreviewImage ? <img src={promptPreviewImage} srcSet={promptPreviewSrcSet || undefined} sizes="(max-width: 767px) calc(100vw - 2rem), (max-width: 1279px) calc(50vw - 2rem), 420px" alt="" aria-hidden="true" className="h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" /> : null}
<div className="absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.72))]" />
<div className="absolute left-4 top-4 flex flex-wrap gap-2">
<span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]">Prompt template</span>
@@ -260,6 +275,7 @@ export default function AcademyList({ pageType, title, description, seo, items,
const [pagination, setPagination] = React.useState({
currentPage: Number(items?.current_page || 1),
lastPage: Number(items?.last_page || 1),
prevPageUrl: items?.prev_page_url || null,
nextPageUrl: items?.next_page_url || null,
})
const [loadingMore, setLoadingMore] = React.useState(false)
@@ -270,12 +286,14 @@ export default function AcademyList({ pageType, title, description, seo, items,
setPagination({
currentPage: Number(items?.current_page || 1),
lastPage: Number(items?.last_page || 1),
prevPageUrl: items?.prev_page_url || null,
nextPageUrl: items?.next_page_url || null,
})
setLoadingMore(false)
}, [initialItems, items?.current_page, items?.last_page, items?.next_page_url, pageType])
}, [initialItems, items?.current_page, items?.last_page, items?.next_page_url, items?.prev_page_url, pageType])
const hasMorePages = pageType === 'prompts' && pagination.currentPage < pagination.lastPage && Boolean(pagination.nextPageUrl)
const hasFallbackPagination = pageType === 'prompts' && pagination.lastPage > 1
const loadMore = React.useCallback(async () => {
if (pageType !== 'prompts' || loadingMore || !pagination.nextPageUrl) {
@@ -292,6 +310,7 @@ export default function AcademyList({ pageType, title, description, seo, items,
setPagination({
currentPage: Number(payload?.current_page || pagination.currentPage),
lastPage: Number(payload?.last_page || pagination.lastPage),
prevPageUrl: payload?.prev_page_url || pagination.prevPageUrl,
nextPageUrl: payload?.next_page_url || null,
})
} catch {
@@ -299,7 +318,7 @@ export default function AcademyList({ pageType, title, description, seo, items,
} finally {
setLoadingMore(false)
}
}, [loadingMore, pageType, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl])
}, [loadingMore, pageType, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl, pagination.prevPageUrl])
React.useEffect(() => {
const sentinel = sentinelRef.current
@@ -355,6 +374,26 @@ export default function AcademyList({ pageType, title, description, seo, items,
<div ref={sentinelRef} className="h-10 w-full" aria-hidden="true" />
{loadingMore ? <div className="rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-300">Loading more prompts...</div> : null}
{!hasMorePages && visibleItems.length > initialItems.length ? <div className="rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-400">You have reached the end of the prompt library.</div> : null}
{hasFallbackPagination ? (
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 rounded-[22px] border border-white/10 bg-black/20 px-5 py-4">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Auto-load is primary. Pagination is available as a backup.</div>
<div className="flex flex-wrap items-center gap-3">
{pagination.prevPageUrl ? (
<Link href={pagination.prevPageUrl} preserveScroll className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">
<i className="fa-solid fa-arrow-left text-[10px]" />
Previous
</Link>
) : null}
<span className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-slate-300">Page {pagination.currentPage || 1} of {pagination.lastPage || 1}</span>
{pagination.nextPageUrl ? (
<Link href={pagination.nextPageUrl} preserveScroll className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">
Next
<i className="fa-solid fa-arrow-right text-[10px]" />
</Link>
) : null}
</div>
</div>
) : null}
</div>
) : null}
</>