Current state with latest updates
This commit is contained in:
@@ -363,8 +363,17 @@ final class AcademyBillingController extends Controller
|
||||
/** @var User|null $user */
|
||||
$user = $request->user();
|
||||
$currentTier = $this->access->currentTier($user);
|
||||
$seo = \app(SeoFactory::class)
|
||||
->simplePage(
|
||||
'Academy Subscription Confirmed — Skinbase',
|
||||
'Payment confirmation for your Skinbase Academy subscription.',
|
||||
\route('academy.billing.success'),
|
||||
false,
|
||||
)
|
||||
->toArray();
|
||||
|
||||
return \Inertia\Inertia::render('Academy/Billing/Success', [
|
||||
'seo' => $seo,
|
||||
'message' => 'Payment is being confirmed. Your access will update automatically.',
|
||||
'currentTier' => $currentTier,
|
||||
'isSubscribed' => $user instanceof User ? $this->access->hasActiveAcademySubscription($user) : false,
|
||||
@@ -381,8 +390,17 @@ final class AcademyBillingController extends Controller
|
||||
public function cancel(): \Inertia\Response
|
||||
{
|
||||
\abort_unless((bool) \config('academy.enabled', true), 404);
|
||||
$seo = \app(SeoFactory::class)
|
||||
->simplePage(
|
||||
'Academy Billing Canceled — Skinbase',
|
||||
'Checkout was canceled before starting a Skinbase Academy subscription.',
|
||||
\route('academy.billing.cancel'),
|
||||
false,
|
||||
)
|
||||
->toArray();
|
||||
|
||||
return \Inertia\Inertia::render('Academy/Billing/Cancel', [
|
||||
'seo' => $seo,
|
||||
'message' => 'Checkout was canceled. No payment was made.',
|
||||
'links' => [
|
||||
'pricing' => \route('academy.pricing'),
|
||||
@@ -495,10 +513,19 @@ final class AcademyBillingController extends Controller
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
$subscription = $this->academySubscription($user);
|
||||
$seo = \app(SeoFactory::class)
|
||||
->simplePage(
|
||||
'Academy Subscription Account — Skinbase',
|
||||
'Manage your Skinbase Academy subscription and billing access.',
|
||||
\route('academy.billing.account'),
|
||||
false,
|
||||
)
|
||||
->toArray();
|
||||
|
||||
$activePlan = $this->activePlan($user);
|
||||
|
||||
return \Inertia\Inertia::render('Academy/Billing/Account', [
|
||||
'seo' => $seo,
|
||||
'currentTier' => $this->access->currentTier($user),
|
||||
'isSubscribed' => $this->access->hasActiveAcademySubscription($user),
|
||||
'activePlan' => $activePlan ? [
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Services\Academy\AcademyAccessService;
|
||||
use App\Services\Academy\AcademyInteractionService;
|
||||
use App\Support\AcademyAnalytics\AcademyAnalyticsContentType;
|
||||
use App\Support\Seo\SeoFactory;
|
||||
use App\Support\Seo\SeoDataBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
@@ -44,6 +45,13 @@ final class AcademyChallengeController extends Controller
|
||||
route('academy.challenges.index'),
|
||||
)
|
||||
->toArray();
|
||||
$seo = SeoDataBuilder::fromArray($seo)
|
||||
->breadcrumbs([
|
||||
['name' => 'Academy', 'url' => route('academy.index')],
|
||||
['name' => 'Challenges', 'url' => route('academy.challenges.index')],
|
||||
])
|
||||
->build()
|
||||
->toArray();
|
||||
|
||||
return Inertia::render('Academy/List', [
|
||||
'pageType' => 'challenges',
|
||||
@@ -94,12 +102,24 @@ final class AcademyChallengeController extends Controller
|
||||
'submitted_at' => $submission->submitted_at?->toISOString(),
|
||||
])->values()->all();
|
||||
|
||||
$seo = app(SeoFactory::class)->collectionPage(
|
||||
$challenge->title . ' — Skinbase Academy',
|
||||
Str::limit((string) ($challenge->excerpt ?? $challenge->description ?? ''), 160, '...'),
|
||||
route('academy.challenges.show', ['slug' => $challenge->slug]),
|
||||
$challenge->cover_image,
|
||||
)->toArray();
|
||||
$canonical = route('academy.challenges.show', ['slug' => $challenge->slug]);
|
||||
$description = Str::limit((string) ($challenge->excerpt ?? $challenge->description ?? ''), 160, '...');
|
||||
$seo = SeoDataBuilder::fromArray(
|
||||
app(SeoFactory::class)->collectionPage(
|
||||
$challenge->title . ' — Skinbase Academy',
|
||||
$description,
|
||||
$canonical,
|
||||
$challenge->cover_image,
|
||||
)->toArray()
|
||||
)
|
||||
->breadcrumbs([
|
||||
['name' => 'Academy', 'url' => route('academy.index')],
|
||||
['name' => 'Challenges', 'url' => route('academy.challenges.index')],
|
||||
['name' => (string) $challenge->title, 'url' => $canonical],
|
||||
])
|
||||
->addJsonLd($this->challengeStructuredData($payload, $canonical, $description))
|
||||
->build()
|
||||
->toArray();
|
||||
|
||||
$interaction = $this->interactions->getInteractionState($request->user(), AcademyAnalyticsContentType::CHALLENGE, (int) $challenge->id);
|
||||
|
||||
@@ -128,4 +148,46 @@ final class AcademyChallengeController extends Controller
|
||||
],
|
||||
])->rootView('academy');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function challengeStructuredData(array $payload, string $canonical, string $description): array
|
||||
{
|
||||
$image = trim((string) ($payload['cover_image'] ?? ''));
|
||||
$imageUrl = $image !== '' && preg_match('/^https?:\/\//i', $image) === 1 ? $image : ($image !== '' ? url($image) : null);
|
||||
$requiredTags = array_values((array) ($payload['required_tags'] ?? []));
|
||||
$status = strtolower(trim((string) ($payload['status'] ?? '')));
|
||||
$eventStatus = match ($status) {
|
||||
'scheduled' => 'https://schema.org/EventScheduled',
|
||||
'active', 'voting' => 'https://schema.org/EventInProgress',
|
||||
'completed', 'archived' => 'https://schema.org/EventCompleted',
|
||||
default => null,
|
||||
};
|
||||
|
||||
return array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Event',
|
||||
'name' => (string) ($payload['title'] ?? 'Skinbase Academy challenge'),
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'image' => $imageUrl,
|
||||
'startDate' => $payload['starts_at'] ?? null,
|
||||
'endDate' => $payload['ends_at'] ?? null,
|
||||
'eventStatus' => $eventStatus,
|
||||
'eventAttendanceMode' => 'https://schema.org/OnlineEventAttendanceMode',
|
||||
'location' => [
|
||||
'@type' => 'VirtualLocation',
|
||||
'url' => $canonical,
|
||||
],
|
||||
'organizer' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => config('seo.site_name', 'Skinbase'),
|
||||
'url' => url('/'),
|
||||
],
|
||||
'keywords' => $requiredTags !== [] ? $requiredTags : null,
|
||||
'isAccessibleForFree' => (string) ($payload['access_level'] ?? 'free') === 'free',
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ final class AcademyChallengeSubmissionController extends Controller
|
||||
'Submit to ' . $challenge->title . ' — Skinbase Academy',
|
||||
'Attach one of your artworks to this Academy challenge submission.',
|
||||
route('academy.challenges.submit', ['slug' => $challenge->slug]),
|
||||
null,
|
||||
false,
|
||||
)->toArray();
|
||||
|
||||
return Inertia::render('Academy/ChallengeSubmit', [
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Services\Academy\AcademyCourseNavigationService;
|
||||
use App\Services\Academy\AcademyCourseProgressService;
|
||||
use App\Support\AcademyAnalytics\AcademyAnalyticsContentType;
|
||||
use App\Support\Seo\SeoFactory;
|
||||
use App\Support\Seo\SeoDataBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@@ -37,6 +38,7 @@ final class AcademyCourseController extends Controller
|
||||
'difficulty' => ['nullable', 'string', 'max:40'],
|
||||
'access' => ['nullable', 'string', 'max:40'],
|
||||
]);
|
||||
$hasActiveFilters = filled($filters['difficulty'] ?? null) || filled($filters['access'] ?? null);
|
||||
|
||||
$query = AcademyCourse::query()->published()->ordered();
|
||||
|
||||
@@ -77,6 +79,13 @@ final class AcademyCourseController extends Controller
|
||||
)
|
||||
->toArray();
|
||||
|
||||
if ($hasActiveFilters) {
|
||||
$seo = SeoDataBuilder::fromArray($seo)
|
||||
->indexable(false)
|
||||
->build()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return Inertia::render('Academy/CoursesIndex', [
|
||||
'seo' => $seo,
|
||||
'title' => 'Academy courses',
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Services\Academy\AcademyCourseNavigationService;
|
||||
use App\Services\Academy\AcademyCourseProgressService;
|
||||
use App\Support\AcademyAnalytics\AcademyAnalyticsContentType;
|
||||
use App\Support\Seo\SeoFactory;
|
||||
use App\Support\Seo\SeoDataBuilder;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@@ -52,7 +53,7 @@ final class AcademyCourseLessonController extends Controller
|
||||
->all();
|
||||
|
||||
$payload = $this->access->courseLessonPayload($courseLesson, $request->user(), true);
|
||||
$canonical = route('academy.courses.lessons.show', ['course' => $course->slug, 'lesson' => $lesson->slug]);
|
||||
$canonical = route('academy.lessons.show', ['slug' => $lesson->slug]);
|
||||
$description = Str::limit(trim((string) ($lesson->seo_description ?? $lesson->excerpt ?? 'Skinbase Academy course lesson.')), 160, '...');
|
||||
$seo = app(SeoFactory::class)->academyLessonPage(
|
||||
(string) ($lesson->seo_title ?? ($lesson->title . ' — ' . $course->title)),
|
||||
@@ -70,6 +71,10 @@ final class AcademyCourseLessonController extends Controller
|
||||
$lesson->updated_at?->toAtomString(),
|
||||
(string) $course->title,
|
||||
)->toArray();
|
||||
$seo = SeoDataBuilder::fromArray($seo)
|
||||
->indexable(false)
|
||||
->build()
|
||||
->toArray();
|
||||
|
||||
$interaction = $this->interactions->getInteractionState($request->user(), AcademyAnalyticsContentType::LESSON, (int) $lesson->id);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Services\Academy\AcademyCacheService;
|
||||
use App\Services\Academy\AcademyInteractionService;
|
||||
use App\Support\AcademyAnalytics\AcademyAnalyticsContentType;
|
||||
use App\Support\Seo\SeoFactory;
|
||||
use App\Support\Seo\SeoDataBuilder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -37,6 +38,9 @@ final class AcademyLessonController extends Controller
|
||||
'category' => ['nullable', 'string', 'max:140'],
|
||||
'difficulty' => ['nullable', 'string', 'max:40'],
|
||||
]);
|
||||
$hasActiveFilters = filled($filters['q'] ?? null)
|
||||
|| filled($filters['category'] ?? null)
|
||||
|| filled($filters['difficulty'] ?? null);
|
||||
|
||||
$query = AcademyLesson::query()
|
||||
->with('category')
|
||||
@@ -78,6 +82,13 @@ final class AcademyLessonController extends Controller
|
||||
)
|
||||
->toArray();
|
||||
|
||||
if ($hasActiveFilters) {
|
||||
$seo = SeoDataBuilder::fromArray($seo)
|
||||
->indexable(false)
|
||||
->build()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return Inertia::render('Academy/List', [
|
||||
'pageType' => 'lessons',
|
||||
'title' => 'Academy lessons',
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Services\Academy\AcademyInteractionService;
|
||||
use App\Services\Academy\AcademyPopularityService;
|
||||
use App\Support\AcademyAnalytics\AcademyAnalyticsContentType;
|
||||
use App\Support\Seo\SeoFactory;
|
||||
use App\Support\Seo\SeoDataBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -41,6 +42,10 @@ final class AcademyPromptController extends Controller
|
||||
'difficulty' => ['nullable', 'string', 'max:40'],
|
||||
'tag' => ['nullable', 'string', 'max:60'],
|
||||
]);
|
||||
$hasActiveFilters = filled($filters['q'] ?? null)
|
||||
|| filled($filters['category'] ?? null)
|
||||
|| filled($filters['difficulty'] ?? null)
|
||||
|| filled($filters['tag'] ?? null);
|
||||
|
||||
$query = AcademyPromptTemplate::query()
|
||||
->with('category')
|
||||
@@ -87,6 +92,13 @@ final class AcademyPromptController extends Controller
|
||||
)
|
||||
->toArray();
|
||||
|
||||
if ($hasActiveFilters) {
|
||||
$seo = SeoDataBuilder::fromArray($seo)
|
||||
->indexable(false)
|
||||
->build()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return Inertia::render('Academy/List', [
|
||||
'pageType' => 'prompts',
|
||||
'promptView' => 'library',
|
||||
@@ -203,6 +215,13 @@ final class AcademyPromptController extends Controller
|
||||
)
|
||||
->toArray();
|
||||
|
||||
if ($selectedPeriod['value'] !== '30d') {
|
||||
$seo = SeoDataBuilder::fromArray($seo)
|
||||
->indexable(false)
|
||||
->build()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return Inertia::render('Academy/List', [
|
||||
'pageType' => 'prompts',
|
||||
'promptView' => 'popular',
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Services\Academy\AcademyAccessService;
|
||||
use App\Services\Academy\AcademyInteractionService;
|
||||
use App\Support\AcademyAnalytics\AcademyAnalyticsContentType;
|
||||
use App\Support\Seo\SeoFactory;
|
||||
use App\Support\Seo\SeoDataBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
@@ -44,6 +45,13 @@ final class AcademyPromptPackController extends Controller
|
||||
route('academy.packs.index'),
|
||||
)
|
||||
->toArray();
|
||||
$seo = SeoDataBuilder::fromArray($seo)
|
||||
->breadcrumbs([
|
||||
['name' => 'Academy', 'url' => route('academy.index')],
|
||||
['name' => 'Prompt Packs', 'url' => route('academy.packs.index')],
|
||||
])
|
||||
->build()
|
||||
->toArray();
|
||||
|
||||
return Inertia::render('Academy/List', [
|
||||
'pageType' => 'packs',
|
||||
@@ -79,12 +87,24 @@ final class AcademyPromptPackController extends Controller
|
||||
->firstOrFail();
|
||||
|
||||
$payload = $this->access->packPayload($pack, $request->user(), true);
|
||||
$seo = app(SeoFactory::class)->collectionPage(
|
||||
$pack->title . ' — Skinbase Academy',
|
||||
Str::limit((string) ($pack->excerpt ?? $pack->description ?? ''), 160, '...'),
|
||||
route('academy.packs.show', ['slug' => $pack->slug]),
|
||||
$pack->cover_image,
|
||||
)->toArray();
|
||||
$canonical = route('academy.packs.show', ['slug' => $pack->slug]);
|
||||
$description = Str::limit((string) ($pack->excerpt ?? $pack->description ?? ''), 160, '...');
|
||||
$seo = SeoDataBuilder::fromArray(
|
||||
app(SeoFactory::class)->collectionPage(
|
||||
$pack->title . ' — Skinbase Academy',
|
||||
$description,
|
||||
$canonical,
|
||||
$pack->cover_image,
|
||||
)->toArray()
|
||||
)
|
||||
->breadcrumbs([
|
||||
['name' => 'Academy', 'url' => route('academy.index')],
|
||||
['name' => 'Prompt Packs', 'url' => route('academy.packs.index')],
|
||||
['name' => (string) $pack->title, 'url' => $canonical],
|
||||
])
|
||||
->addJsonLd($this->packStructuredData($payload, $canonical, $description))
|
||||
->build()
|
||||
->toArray();
|
||||
|
||||
$interaction = $this->interactions->getInteractionState($request->user(), AcademyAnalyticsContentType::PROMPT_PACK, (int) $pack->id);
|
||||
|
||||
@@ -112,4 +132,58 @@ final class AcademyPromptPackController extends Controller
|
||||
],
|
||||
])->rootView('academy');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function packStructuredData(array $payload, string $canonical, string $description): array
|
||||
{
|
||||
$image = trim((string) ($payload['cover_image'] ?? ''));
|
||||
$imageUrl = $image !== '' && preg_match('/^https?:\/\//i', $image) === 1 ? $image : ($image !== '' ? url($image) : null);
|
||||
$keywords = array_values((array) ($payload['tags'] ?? []));
|
||||
$isFree = (string) ($payload['access_level'] ?? 'free') === 'free';
|
||||
$promptEntries = collect((array) ($payload['prompts'] ?? []))
|
||||
->map(function (array $prompt): ?array {
|
||||
$title = trim((string) ($prompt['title'] ?? ''));
|
||||
$slug = trim((string) ($prompt['slug'] ?? ''));
|
||||
|
||||
if ($title === '' || $slug === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'@type' => 'ListItem',
|
||||
'position' => null,
|
||||
'item' => [
|
||||
'@type' => 'CreativeWork',
|
||||
'name' => $title,
|
||||
'url' => route('academy.prompts.show', ['slug' => $slug]),
|
||||
],
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->map(function (array $item, int $index): array {
|
||||
$item['position'] = $index + 1;
|
||||
|
||||
return $item;
|
||||
})
|
||||
->all();
|
||||
|
||||
return array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => ['CreativeWork', 'LearningResource'],
|
||||
'name' => (string) ($payload['title'] ?? 'Skinbase Academy prompt pack'),
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'image' => $imageUrl,
|
||||
'keywords' => $keywords !== [] ? $keywords : null,
|
||||
'isAccessibleForFree' => $isFree,
|
||||
'hasPart' => $promptEntries !== [] ? [
|
||||
'@type' => 'ItemList',
|
||||
'itemListElement' => $promptEntries,
|
||||
] : null,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user