Implement academy analytics, billing, and web stories updates
This commit is contained in:
@@ -75,21 +75,29 @@
|
||||
|
||||
return array_filter([
|
||||
'name' => $name,
|
||||
'url' => $username !== '' ? url('/@' . ltrim($username, '@')) : null,
|
||||
]);
|
||||
'url' => $username !== '' ? route('profile.show', ['username' => $username]) : null,
|
||||
], fn ($value) => $value !== null && $value !== '');
|
||||
};
|
||||
|
||||
// Ensure we always provide a top-level author object for structured data.
|
||||
$topAuthor = $makeForumAuthor($author ?? $opPost?->user ?? null);
|
||||
$topAuthor = $makeForumAuthor($author ?? $opPost?->user ?? $thread->user ?? null);
|
||||
if (! $topAuthor) {
|
||||
$topAuthor = ['name' => (string) ($opPost?->user?->name ?? $thread->user?->name ?? 'Skinbase')];
|
||||
$topAuthor = ['name' => (string) (config('app.name') ?: 'Skinbase')];
|
||||
}
|
||||
|
||||
$threadText = $threadDescription;
|
||||
if ($threadText === null || $threadText === '') {
|
||||
$threadBody = trim((string) strip_tags((string) ($thread->content ?? '')));
|
||||
$threadText = $threadBody !== ''
|
||||
? Str::limit($threadBody, 220)
|
||||
: Str::limit((string) $thread->title, 220);
|
||||
}
|
||||
|
||||
$forumMicrodata = [
|
||||
'kind' => 'topic',
|
||||
'canonical' => route('forum.thread.show', ['thread' => $thread->id, 'slug' => $thread->slug]),
|
||||
'title' => (string) $thread->title,
|
||||
'text' => $threadDescription,
|
||||
'text' => $threadText,
|
||||
'date_published' => $thread->created_at?->toIso8601String(),
|
||||
'date_modified' => ($thread->last_post_at ?? $thread->updated_at)?->toIso8601String(),
|
||||
'comment_count' => (int) ($reply_count ?? 0),
|
||||
@@ -119,7 +127,7 @@
|
||||
'text' => Str::limit($text, 300),
|
||||
'date_published' => $post->created_at?->toIso8601String(),
|
||||
'date_modified' => ($post->edited_at ?? $post->created_at)?->toIso8601String(),
|
||||
'author' => $makeForumAuthor($post->user ?? null) ?: ['name' => (string) ($post->user?->name ?? 'Skinbase')],
|
||||
'author' => $makeForumAuthor($post->user ?? null) ?: $topAuthor,
|
||||
], fn ($value) => $value !== null && $value !== '');
|
||||
})
|
||||
->values()
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
'thumbnailUrl' => $article->cover_mobile_url,
|
||||
'caption' => $article->title,
|
||||
'creditText' => $articleImageCreditText,
|
||||
'copyrightNotice' => $articleImageCreditText,
|
||||
'license' => $articleImageLicenseUrl,
|
||||
'acquireLicensePage' => $articleImageLicenseUrl,
|
||||
'creator' => [
|
||||
@@ -63,8 +64,14 @@
|
||||
'thumbnailUrl' => $article->cover_mobile_url,
|
||||
'caption' => $article->title,
|
||||
'creditText' => $articleImageCreditText,
|
||||
'copyrightNotice' => $articleImageCreditText,
|
||||
'license' => $articleImageLicenseUrl,
|
||||
'acquireLicensePage' => $articleImageLicenseUrl,
|
||||
'creator' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $articleImageCreditText,
|
||||
'url' => url('/'),
|
||||
],
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '')
|
||||
: null,
|
||||
'datePublished' => $article->published_at?->toIso8601String(),
|
||||
@@ -73,6 +80,9 @@
|
||||
'author' => array_filter([
|
||||
'@type' => 'Person',
|
||||
'name' => $article->author?->name,
|
||||
'url' => $article->author?->username
|
||||
? route('profile.show', ['username' => $article->author->username])
|
||||
: null,
|
||||
]),
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
@if(!empty($forumMicrodata['canonical']))<meta itemprop="mainEntityOfPage" content="{{ $forumMicrodata['canonical'] }}" />@endif
|
||||
@if(!empty($forumMicrodata['canonical']))<meta itemprop="url" content="{{ $forumMicrodata['canonical'] }}" />@endif
|
||||
@if(!empty($forumMicrodata['title']))<meta itemprop="headline" content="{{ $forumMicrodata['title'] }}" />@endif
|
||||
@if(!empty($forumMicrodata['text']))<meta itemprop="text" content="{{ $forumMicrodata['text'] }}" />@endif
|
||||
@php
|
||||
$topicText = trim((string) ($forumMicrodata['text'] ?? ''));
|
||||
if ($topicText === '') {
|
||||
$topicText = trim((string) ($forumMicrodata['title'] ?? ''));
|
||||
}
|
||||
@endphp
|
||||
@if($topicText !== '')<meta itemprop="text" content="{{ $topicText }}" />@endif
|
||||
@if(!empty($forumMicrodata['date_published']))<meta itemprop="datePublished" content="{{ $forumMicrodata['date_published'] }}" />@endif
|
||||
@if(!empty($forumMicrodata['date_modified']))<meta itemprop="dateModified" content="{{ $forumMicrodata['date_modified'] }}" />@endif
|
||||
@if(isset($forumMicrodata['comment_count']))<meta itemprop="commentCount" content="{{ (int) $forumMicrodata['comment_count'] }}" />@endif
|
||||
@@ -74,20 +80,38 @@
|
||||
@if(!empty($forumMicrodata['list_name']))<meta itemprop="name" content="{{ $forumMicrodata['list_name'] }}" />@endif
|
||||
|
||||
@foreach(($forumMicrodata['items'] ?? []) as $index => $item)
|
||||
@php
|
||||
$itemType = $item['type'] ?? 'WebPage';
|
||||
$itemText = trim((string) ($item['text'] ?? ''));
|
||||
$itemAuthor = $item['author'] ?? null;
|
||||
|
||||
if ($itemType === 'DiscussionForumPosting') {
|
||||
if ($itemText === '') {
|
||||
$itemText = trim((string) ($item['description'] ?? $item['title'] ?? ''));
|
||||
}
|
||||
|
||||
if (empty($itemAuthor)) {
|
||||
$fallbackAuthorName = trim((string) ($item['uname'] ?? $item['author_name'] ?? ''));
|
||||
if ($fallbackAuthorName !== '') {
|
||||
$itemAuthor = ['name' => $fallbackAuthorName];
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
<div itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
||||
<meta itemprop="position" content="{{ $index + 1 }}" />
|
||||
<div itemprop="item" itemscope itemtype="https://schema.org/{{ $item['type'] ?? 'WebPage' }}">
|
||||
<div itemprop="item" itemscope itemtype="https://schema.org/{{ $itemType }}">
|
||||
@if(!empty($item['url']))<meta itemprop="url" content="{{ $item['url'] }}" />@endif
|
||||
@if(!empty($item['title']))<meta itemprop="{{ ($item['type'] ?? null) === 'DiscussionForumPosting' ? 'headline' : 'name' }}" content="{{ $item['title'] }}" />@endif
|
||||
@if(!empty($item['title']))<meta itemprop="{{ $itemType === 'DiscussionForumPosting' ? 'headline' : 'name' }}" content="{{ $item['title'] }}" />@endif
|
||||
@if(!empty($item['description']))<meta itemprop="description" content="{{ $item['description'] }}" />@endif
|
||||
@if(!empty($item['text']))<meta itemprop="text" content="{{ $item['text'] }}" />@endif
|
||||
@if(!empty($itemText))<meta itemprop="text" content="{{ $itemText }}" />@endif
|
||||
@if(!empty($item['date_published']))<meta itemprop="datePublished" content="{{ $item['date_published'] }}" />@endif
|
||||
@if(isset($item['comment_count']))<meta itemprop="commentCount" content="{{ (int) $item['comment_count'] }}" />@endif
|
||||
@if(!empty($item['date_modified']))<meta itemprop="dateModified" content="{{ $item['date_modified'] }}" />@endif
|
||||
@if(!empty($item['author']))
|
||||
@if(!empty($itemAuthor))
|
||||
<div itemprop="author" itemscope itemtype="https://schema.org/Person">
|
||||
@if(!empty($item['author']['url']))<meta itemprop="url" content="{{ $item['author']['url'] }}" />@endif
|
||||
@if(!empty($item['author']['name']))<meta itemprop="name" content="{{ $item['author']['name'] }}" />@endif
|
||||
@if(!empty($itemAuthor['url']))<meta itemprop="url" content="{{ $itemAuthor['url'] }}" />@endif
|
||||
@if(!empty($itemAuthor['name']))<meta itemprop="name" content="{{ $itemAuthor['name'] }}" />@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
55
resources/views/web-stories/index.blade.php
Normal file
55
resources/views/web-stories/index.blade.php
Normal file
@@ -0,0 +1,55 @@
|
||||
@extends('layouts.nova.content-layout')
|
||||
|
||||
@php
|
||||
$hero_title = 'Skinbase Web Stories';
|
||||
$hero_description = 'Explore visual stories from Skinbase Worlds, creator features, seasonal collections, and digital art highlights.';
|
||||
@endphp
|
||||
|
||||
@section('page-content')
|
||||
@if($stories->count() > 0)
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
@foreach($stories as $story)
|
||||
<a href="{{ route('web-stories.show', ['slug' => $story->slug]) }}" class="group overflow-hidden rounded-[28px] border border-white/10 bg-white/[0.03] transition hover:bg-white/[0.06]">
|
||||
<div class="aspect-[3/4] overflow-hidden bg-black/30">
|
||||
@if($story->posterPortraitUrl())
|
||||
<img src="{{ $story->posterPortraitUrl() }}" alt="{{ $story->title }}" class="h-full w-full object-cover transition duration-500 group-hover:scale-[1.03]" loading="lazy">
|
||||
@else
|
||||
<div class="flex h-full items-center justify-center text-white/20">
|
||||
<i class="fa-solid fa-book-open-reader text-5xl"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-300/80">Web Story</div>
|
||||
<h2 class="mt-3 line-clamp-2 text-xl font-semibold tracking-[-0.03em] text-white">{{ $story->title }}</h2>
|
||||
@if($story->excerpt)
|
||||
<p class="mt-3 line-clamp-3 text-sm leading-6 text-slate-300">{{ $story->excerpt }}</p>
|
||||
@endif
|
||||
<div class="mt-4 flex flex-wrap items-center gap-2 text-xs uppercase tracking-[0.16em] text-slate-400">
|
||||
@if($story->world)
|
||||
<span class="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1">{{ $story->world->title }}</span>
|
||||
@endif
|
||||
@if($story->published_at)
|
||||
<time datetime="{{ $story->published_at->toIso8601String() }}">{{ $story->published_at->format('M j, Y') }}</time>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-5 inline-flex items-center gap-2 text-sm font-semibold text-sky-300 transition group-hover:text-sky-200">
|
||||
View Story
|
||||
<i class="fa-solid fa-arrow-right text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-10 flex justify-center">
|
||||
{{ $stories->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-[28px] border border-white/10 bg-white/[0.03] px-8 py-14 text-center">
|
||||
<div class="text-white/20"><i class="fa-solid fa-book-open-reader text-5xl"></i></div>
|
||||
<h2 class="mt-4 text-xl font-semibold text-white">No Web Stories published yet</h2>
|
||||
<p class="mt-3 text-sm leading-6 text-slate-300">Published Skinbase Web Stories will appear here once they are ready.</p>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
156
resources/views/web-stories/show.blade.php
Normal file
156
resources/views/web-stories/show.blade.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<!doctype html>
|
||||
<html amp lang="{{ app()->getLocale() }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ $meta['title'] }}</title>
|
||||
<link rel="canonical" href="{{ $meta['canonical'] }}">
|
||||
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
|
||||
<meta name="description" content="{{ $meta['description'] }}">
|
||||
<meta name="robots" content="{{ $meta['robots'] }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:title" content="{{ $meta['og_title'] }}">
|
||||
<meta property="og:description" content="{{ $meta['og_description'] }}">
|
||||
<meta property="og:url" content="{{ $meta['og_url'] }}">
|
||||
<meta property="og:image" content="{{ $meta['og_image'] }}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ $meta['twitter_title'] }}">
|
||||
<meta name="twitter:description" content="{{ $meta['twitter_description'] }}">
|
||||
<meta name="twitter:image" content="{{ $meta['twitter_image'] }}">
|
||||
<script async src="https://cdn.ampproject.org/v0.js"></script>
|
||||
<script async custom-element="amp-story" src="https://cdn.ampproject.org/v0/amp-story-1.0.js"></script>
|
||||
<script async custom-element="amp-video" src="https://cdn.ampproject.org/v0/amp-video-0.1.js"></script>
|
||||
<style amp-boilerplate>
|
||||
body {
|
||||
-webkit-animation: -amp-start 8s steps(1,end) 0s 1 normal both;
|
||||
-moz-animation: -amp-start 8s steps(1,end) 0s 1 normal both;
|
||||
-ms-animation: -amp-start 8s steps(1,end) 0s 1 normal both;
|
||||
animation: -amp-start 8s steps(1,end) 0s 1 normal both;
|
||||
}
|
||||
@-webkit-keyframes -amp-start { from { visibility: hidden; } to { visibility: visible; } }
|
||||
@-moz-keyframes -amp-start { from { visibility: hidden; } to { visibility: visible; } }
|
||||
@-ms-keyframes -amp-start { from { visibility: hidden; } to { visibility: visible; } }
|
||||
@-o-keyframes -amp-start { from { visibility: hidden; } to { visibility: visible; } }
|
||||
@keyframes -amp-start { from { visibility: hidden; } to { visibility: visible; } }
|
||||
</style>
|
||||
<noscript>
|
||||
<style amp-boilerplate>
|
||||
body {
|
||||
-webkit-animation: none;
|
||||
-moz-animation: none;
|
||||
-ms-animation: none;
|
||||
animation: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
<style amp-custom>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #020617;
|
||||
}
|
||||
.story-text {
|
||||
color: #ffffff;
|
||||
padding: 32px;
|
||||
text-shadow: 0 2px 18px rgba(0,0,0,.65);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
min-height: 100%;
|
||||
}
|
||||
.story-text--top {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.story-text--center {
|
||||
justify-content: center;
|
||||
}
|
||||
.story-kicker {
|
||||
font-size: 13px;
|
||||
letter-spacing: .16em;
|
||||
text-transform: uppercase;
|
||||
opacity: .85;
|
||||
}
|
||||
.story-title {
|
||||
font-size: 34px;
|
||||
line-height: 1.05;
|
||||
font-weight: 800;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.story-body {
|
||||
font-size: 17px;
|
||||
line-height: 1.35;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.story-cta {
|
||||
display: inline-block;
|
||||
margin-top: 18px;
|
||||
padding: 11px 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.92);
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
.overlay {
|
||||
background: linear-gradient(to top, rgba(0,0,0,.72), rgba(0,0,0,.12), rgba(0,0,0,.12));
|
||||
}
|
||||
.gradient-fill {
|
||||
background: linear-gradient(180deg, rgba(15,23,42,0.95) 0%, rgba(14,165,233,0.35) 100%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<amp-story
|
||||
standalone
|
||||
title="{{ $story->title }}"
|
||||
publisher="Skinbase"
|
||||
publisher-logo-src="{{ $story->publisherLogoUrl() }}"
|
||||
poster-portrait-src="{{ $story->posterPortraitUrl() }}"
|
||||
@if($story->posterSquareUrl()) poster-square-src="{{ $story->posterSquareUrl() }}" @endif
|
||||
>
|
||||
@foreach($story->orderedPages->where('active', true) as $page)
|
||||
<amp-story-page id="page-{{ $page->position }}">
|
||||
<amp-story-grid-layer template="fill">
|
||||
@if($page->background_type === \App\Models\WorldWebStoryPage::BACKGROUND_VIDEO && $page->backgroundUrl())
|
||||
<amp-video autoplay loop muted layout="fill" poster="{{ $page->desktopBackgroundUrl() ?: $page->backgroundUrl() }}">
|
||||
<source src="{{ $page->backgroundUrl() }}" type="video/mp4">
|
||||
</amp-video>
|
||||
@elseif($page->background_type === \App\Models\WorldWebStoryPage::BACKGROUND_GRADIENT || ! $page->backgroundUrl())
|
||||
<div class="gradient-fill"></div>
|
||||
@else
|
||||
<amp-img
|
||||
src="{{ $page->backgroundUrl() }}"
|
||||
width="1080"
|
||||
height="1920"
|
||||
layout="responsive"
|
||||
alt="{{ $page->alt_text ?: ($page->headline ?: $story->title) }}"
|
||||
></amp-img>
|
||||
@endif
|
||||
</amp-story-grid-layer>
|
||||
|
||||
<amp-story-grid-layer template="fill">
|
||||
<div class="overlay"></div>
|
||||
</amp-story-grid-layer>
|
||||
|
||||
<amp-story-grid-layer template="vertical" class="story-text story-text--{{ $page->text_position ?: 'bottom' }}">
|
||||
@if($page->caption)
|
||||
<div class="story-kicker">{{ $page->caption }}</div>
|
||||
@endif
|
||||
|
||||
@if($page->headline)
|
||||
<h1 class="story-title">{{ $page->headline }}</h1>
|
||||
@endif
|
||||
|
||||
@if($page->body)
|
||||
<p class="story-body">{{ $page->body }}</p>
|
||||
@endif
|
||||
|
||||
@if($page->cta_label && $page->cta_url)
|
||||
<a class="story-cta" href="{{ $page->cta_url }}">{{ $page->cta_label }}</a>
|
||||
@endif
|
||||
</amp-story-grid-layer>
|
||||
</amp-story-page>
|
||||
@endforeach
|
||||
</amp-story>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user