diff --git a/app/Http/Controllers/Settings/AcademyAdminController.php b/app/Http/Controllers/Settings/AcademyAdminController.php
index c96cfff0..2e5e1de5 100644
--- a/app/Http/Controllers/Settings/AcademyAdminController.php
+++ b/app/Http/Controllers/Settings/AcademyAdminController.php
@@ -78,6 +78,7 @@ final class AcademyAdminController extends Controller
'challenges' => route('admin.academy.challenges.index'),
'submissions' => route('admin.academy.submissions.index'),
'badges' => route('admin.academy.badges.index'),
+ 'analytics' => route('admin.academy.analytics.overview'),
],
]);
}
@@ -1812,6 +1813,7 @@ final class AcademyAdminController extends Controller
->map(function ($note): ?array {
if (is_string($note)) {
$normalized = [
+ 'display_type' => '',
'provider' => '',
'model_name' => '',
'notes' => trim($note),
@@ -1833,6 +1835,7 @@ final class AcademyAdminController extends Controller
}
$normalized = [
+ 'display_type' => trim((string) ($note['display_type'] ?? '')),
'provider' => trim((string) ($note['provider'] ?? '')),
'model_name' => trim((string) ($note['model_name'] ?? '')),
'notes' => trim((string) ($note['notes'] ?? '')),
@@ -1847,6 +1850,7 @@ final class AcademyAdminController extends Controller
];
$hasContent = collect([
+ $normalized['display_type'],
$normalized['provider'],
$normalized['model_name'],
$normalized['notes'],
@@ -1890,7 +1894,9 @@ final class AcademyAdminController extends Controller
$previousPaths
->reject(fn (string $path): bool => in_array($path, $nextPaths, true))
- ->each(fn (string $path): bool => $this->deleteStoredLessonMediaIfLocal($path));
+ ->each(function (string $path): void {
+ $this->deleteStoredLessonMediaIfLocal($path);
+ });
}
private function promptPreviewImageUpload(UpsertAcademyPromptTemplateRequest $request): ?UploadedFile
diff --git a/config/sitemaps.php b/config/sitemaps.php
index c20c687c..a386bd3d 100644
--- a/config/sitemaps.php
+++ b/config/sitemaps.php
@@ -83,16 +83,13 @@ return [
'enabled' => [
'artworks',
- 'academy-lessons',
- 'academy-prompts',
- 'academy-packs',
- 'academy-challenges',
'users',
'tags',
'categories',
'collections',
'cards',
'stories',
+ 'web-stories',
'news',
'news-google',
'forum-index',
diff --git a/resources/js/Pages/Academy/List.jsx b/resources/js/Pages/Academy/List.jsx
index d3278279..4a723eb8 100644
--- a/resources/js/Pages/Academy/List.jsx
+++ b/resources/js/Pages/Academy/List.jsx
@@ -2,6 +2,7 @@ import React from 'react'
import { Link, router, usePage } from '@inertiajs/react'
import SeoHead from '../../components/seo/SeoHead'
import NovaSelect from '../../components/ui/NovaSelect'
+import { trackAcademySearchResultClick, trackUpgradeClick, useAcademyPageAnalytics } from '../../lib/academyAnalytics'
function academyHref(section, slug) {
return `/academy/${section}/${encodeURIComponent(slug)}`
@@ -65,7 +66,15 @@ function itemHref(pageType, item) {
return academyHref('challenges', item.slug)
}
-function PromptLibraryHero({ title, description, items, pricingUrl }) {
+function searchResultContentType(pageType) {
+ if (pageType === 'prompts') return 'academy_prompt'
+ if (pageType === 'lessons') return 'academy_lesson'
+ if (pageType === 'packs') return 'academy_prompt_pack'
+ if (pageType === 'challenges') return 'academy_challenge'
+ return null
+}
+
+function PromptLibraryHero({ title, description, items, pricingUrl, totalCount }) {
const featuredImages = (items || [])
.map((item) => item?.preview_image)
.filter(Boolean)
@@ -102,7 +111,7 @@ function PromptLibraryHero({ title, description, items, pricingUrl }) {
Upgrade preview
- {items?.length || 0} prompts in view
+ {totalCount || 0} prompts available
@@ -134,13 +143,35 @@ function PromptLibraryHero({ title, description, items, pricingUrl }) {
)
}
-function AcademyCard({ pageType, item }) {
+function AcademyCard({ pageType, item, analytics, searchContext, position }) {
const lessonSeries = String(item?.series_name || '').trim()
const promptPreviewImage = item?.preview_image || ''
+ const contentType = searchResultContentType(pageType)
+ const href = itemHref(pageType, item)
+ const trackSearchClick = () => {
+ if (!searchContext?.query || !contentType) {
+ return
+ }
+
+ trackAcademySearchResultClick(analytics, searchContext, {
+ contentType,
+ contentId: item?.id,
+ position,
+ })
+ }
if (pageType === 'prompts') {
return (
-
+
{promptPreviewImage ?

: null}
@@ -170,7 +201,16 @@ function AcademyCard({ pageType, item }) {
}
return (
-
+
{pageType.slice(0, -1)}
@@ -190,16 +230,101 @@ function AcademyCard({ pageType, item }) {
)
}
-export default function AcademyList({ pageType, title, description, seo, items, filters, categories, pricingUrl }) {
+async function fetchAcademyPage(url) {
+ const response = await fetch(url, {
+ headers: {
+ Accept: 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ credentials: 'same-origin',
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to load the next page.')
+ }
+
+ return response.json()
+}
+
+export default function AcademyList({ pageType, title, description, seo, items, filters, categories, pricingUrl, analytics }) {
const flash = usePage().props.flash || {}
- const visibleItems = Array.isArray(items?.data) ? items.data : []
+ useAcademyPageAnalytics(analytics)
+ const searchContext = analytics?.search ? {
+ query: analytics.search.query,
+ normalizedQuery: analytics.search.normalizedQuery,
+ resultsCount: analytics.search.resultsCount,
+ filters,
+ } : null
+ const initialItems = React.useMemo(() => (Array.isArray(items?.data) ? items.data : []), [items])
+ const [visibleItems, setVisibleItems] = React.useState(initialItems)
+ const [pagination, setPagination] = React.useState({
+ currentPage: Number(items?.current_page || 1),
+ lastPage: Number(items?.last_page || 1),
+ nextPageUrl: items?.next_page_url || null,
+ })
+ const [loadingMore, setLoadingMore] = React.useState(false)
+ const sentinelRef = React.useRef(null)
+
+ React.useEffect(() => {
+ setVisibleItems(initialItems)
+ setPagination({
+ currentPage: Number(items?.current_page || 1),
+ lastPage: Number(items?.last_page || 1),
+ nextPageUrl: items?.next_page_url || null,
+ })
+ setLoadingMore(false)
+ }, [initialItems, items?.current_page, items?.last_page, items?.next_page_url, pageType])
+
+ const hasMorePages = pageType === 'prompts' && pagination.currentPage < pagination.lastPage && Boolean(pagination.nextPageUrl)
+
+ const loadMore = React.useCallback(async () => {
+ if (pageType !== 'prompts' || loadingMore || !pagination.nextPageUrl) {
+ return
+ }
+
+ setLoadingMore(true)
+
+ try {
+ const payload = await fetchAcademyPage(pagination.nextPageUrl)
+ const nextItems = Array.isArray(payload?.data) ? payload.data : []
+
+ setVisibleItems((current) => [...current, ...nextItems.filter((item) => !current.some((existing) => String(existing.id) === String(item.id)))])
+ setPagination({
+ currentPage: Number(payload?.current_page || pagination.currentPage),
+ lastPage: Number(payload?.last_page || pagination.lastPage),
+ nextPageUrl: payload?.next_page_url || null,
+ })
+ } catch {
+ setPagination((current) => ({ ...current, nextPageUrl: null }))
+ } finally {
+ setLoadingMore(false)
+ }
+ }, [loadingMore, pageType, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl])
+
+ React.useEffect(() => {
+ const sentinel = sentinelRef.current
+
+ if (!sentinel || !hasMorePages || loadingMore || typeof window === 'undefined' || typeof window.IntersectionObserver !== 'function') {
+ return undefined
+ }
+
+ const observer = new window.IntersectionObserver((entries) => {
+ if (entries[0]?.isIntersecting) {
+ void loadMore()
+ }
+ }, { rootMargin: '360px 0px' })
+
+ observer.observe(sentinel)
+
+ return () => observer.disconnect()
+ }, [hasMorePages, loadMore, loadingMore])
return (
- {pageType === 'prompts' ?
: (
+ {pageType === 'prompts' ?
: (
@@ -207,7 +332,7 @@ export default function AcademyList({ pageType, title, description, seo, items,
{title}
{description}
-
Upgrade preview
+
trackUpgradeClick(analytics, { source: `${pageType}_list_hero` })} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">Upgrade preview
)}
@@ -220,9 +345,19 @@ export default function AcademyList({ pageType, title, description, seo, items,
{visibleItems.length === 0 ? (
Nothing matched this Academy view yet.
) : (
-
- {visibleItems.map((item) => )}
-
+ <>
+
+ {visibleItems.map((item, index) => )}
+
+
+ {pageType === 'prompts' ? (
+
+
+ {loadingMore ?
Loading more prompts...
: null}
+ {!hasMorePages && visibleItems.length > initialItems.length ?
You have reached the end of the prompt library.
: null}
+
+ ) : null}
+ >
)}