update
This commit is contained in:
@@ -8,18 +8,20 @@ use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\StoryComment;
|
||||
use App\Models\Story;
|
||||
use App\Models\StoryTag;
|
||||
use App\Models\StoryView;
|
||||
use App\Models\User;
|
||||
use App\Notifications\StoryStatusNotification;
|
||||
use App\Services\SocialService;
|
||||
use App\Services\StoryPublicationService;
|
||||
use App\Support\AvatarUrl;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -88,7 +90,7 @@ class StoryController extends Controller
|
||||
public function show(Request $request, string $slug): View
|
||||
{
|
||||
$story = Story::published()
|
||||
->with(['creator.profile', 'tags'])
|
||||
->with(['creator.profile', 'creator.statistics', 'tags'])
|
||||
->where('slug', $slug)
|
||||
->firstOrFail();
|
||||
|
||||
@@ -127,28 +129,49 @@ class StoryController extends Controller
|
||||
->get(['id', 'title', 'slug']);
|
||||
}
|
||||
|
||||
$discussionComments = collect();
|
||||
if ($story->creator_id !== null && Schema::hasTable('profile_comments')) {
|
||||
$discussionComments = DB::table('profile_comments as pc')
|
||||
->join('users as u', 'u.id', '=', 'pc.author_user_id')
|
||||
->where('pc.profile_user_id', $story->creator_id)
|
||||
->where('pc.is_active', true)
|
||||
->orderByDesc('pc.created_at')
|
||||
->limit(8)
|
||||
->get([
|
||||
'pc.id',
|
||||
'pc.body',
|
||||
'pc.created_at',
|
||||
'u.username as author_username',
|
||||
]);
|
||||
}
|
||||
$social = app(SocialService::class);
|
||||
$initialComments = Schema::hasTable('story_comments')
|
||||
? StoryComment::query()
|
||||
->with(['user.profile', 'approvedReplies'])
|
||||
->where('story_id', $story->id)
|
||||
->where('is_approved', true)
|
||||
->whereNull('parent_id')
|
||||
->whereNull('deleted_at')
|
||||
->latest('created_at')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn (StoryComment $comment) => $social->formatComment($comment, $request->user()?->id, true))
|
||||
->values()
|
||||
->all()
|
||||
: [];
|
||||
|
||||
$storyState = $social->storyStateFor($request->user(), $story);
|
||||
|
||||
$storySocialProps = [
|
||||
'story' => [
|
||||
'id' => (int) $story->id,
|
||||
'slug' => (string) $story->slug,
|
||||
'title' => (string) $story->title,
|
||||
],
|
||||
'creator' => $story->creator ? [
|
||||
'id' => (int) $story->creator->id,
|
||||
'username' => (string) ($story->creator->username ?? ''),
|
||||
'display_name' => (string) ($story->creator->name ?: $story->creator->username ?: 'Creator'),
|
||||
'avatar_url' => AvatarUrl::forUser((int) $story->creator->id, $story->creator->profile?->avatar_hash, 128),
|
||||
'followers_count' => (int) ($story->creator->statistics?->followers_count ?? 0),
|
||||
'profile_url' => $story->creator->username ? '/@' . $story->creator->username : null,
|
||||
] : null,
|
||||
'state' => $storyState,
|
||||
'comments' => $initialComments,
|
||||
'is_authenticated' => $request->user() !== null,
|
||||
];
|
||||
|
||||
return view('web.stories.show', [
|
||||
'story' => $story,
|
||||
'safeContent' => $storyContentHtml,
|
||||
'relatedStories' => $relatedStories,
|
||||
'relatedArtworks' => $relatedArtworks,
|
||||
'comments' => $discussionComments,
|
||||
'storySocialProps' => $storySocialProps,
|
||||
'page_title' => $story->title . ' - Skinbase Stories',
|
||||
'page_meta_description' => $story->excerpt ?: Str::limit(strip_tags((string) $story->content), 160),
|
||||
'page_canonical' => route('stories.show', $story->slug),
|
||||
@@ -212,6 +235,10 @@ class StoryController extends Controller
|
||||
|
||||
$story->tags()->sync($this->resolveTagIds($validated));
|
||||
|
||||
if ($resolved['status'] === 'published') {
|
||||
app(StoryPublicationService::class)->afterPersistence($story, 'published', false);
|
||||
}
|
||||
|
||||
if ($resolved['status'] === 'published') {
|
||||
return redirect()->route('stories.show', ['slug' => $story->slug])
|
||||
->with('status', 'Story published.');
|
||||
@@ -275,6 +302,8 @@ class StoryController extends Controller
|
||||
{
|
||||
abort_unless($this->canManageStory($request, $story), 403);
|
||||
|
||||
$wasPublished = $story->published_at !== null || $story->status === 'published';
|
||||
|
||||
$validated = $this->validateStoryPayload($request);
|
||||
$resolved = $this->resolveWorkflowState($request, $validated, false);
|
||||
$serializedContent = $this->normalizeStoryContent($validated['content'] ?? []);
|
||||
@@ -302,6 +331,10 @@ class StoryController extends Controller
|
||||
|
||||
$story->tags()->sync($this->resolveTagIds($validated));
|
||||
|
||||
if (! $wasPublished && $resolved['status'] === 'published') {
|
||||
app(StoryPublicationService::class)->afterPersistence($story, 'published', false);
|
||||
}
|
||||
|
||||
return back()->with('status', 'Story updated.');
|
||||
}
|
||||
|
||||
@@ -370,14 +403,10 @@ class StoryController extends Controller
|
||||
{
|
||||
abort_unless($this->canManageStory($request, $story), 403);
|
||||
|
||||
$story->update([
|
||||
'status' => 'published',
|
||||
app(StoryPublicationService::class)->publish($story, 'published', [
|
||||
'published_at' => now(),
|
||||
'scheduled_for' => null,
|
||||
]);
|
||||
|
||||
$story->creator?->notify(new StoryStatusNotification($story, 'published'));
|
||||
|
||||
return redirect()->route('stories.show', ['slug' => $story->slug])->with('status', 'Story published.');
|
||||
}
|
||||
|
||||
@@ -512,11 +541,19 @@ class StoryController extends Controller
|
||||
$story->tags()->sync($this->resolveTagIds(['tags_csv' => $validated['tags_csv']]));
|
||||
}
|
||||
|
||||
if ($workflow['status'] === 'published') {
|
||||
app(StoryPublicationService::class)->afterPersistence($story, 'published', false);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'story_id' => (int) $story->id,
|
||||
'status' => $story->status,
|
||||
'message' => 'Story created.',
|
||||
'edit_url' => route('creator.stories.edit', ['story' => $story->id]),
|
||||
'preview_url' => route('creator.stories.preview', ['story' => $story->id]),
|
||||
'analytics_url' => route('creator.stories.analytics', ['story' => $story->id]),
|
||||
'public_url' => route('stories.show', ['slug' => $story->slug]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -540,6 +577,7 @@ class StoryController extends Controller
|
||||
|
||||
$story = Story::query()->findOrFail((int) $validated['story_id']);
|
||||
abort_unless($this->canManageStory($request, $story), 403);
|
||||
$wasPublished = $story->published_at !== null || $story->status === 'published';
|
||||
|
||||
$workflow = $this->resolveWorkflowState($request, array_merge([
|
||||
'status' => $story->status,
|
||||
@@ -576,11 +614,19 @@ class StoryController extends Controller
|
||||
$story->tags()->sync($this->resolveTagIds(['tags_csv' => $validated['tags_csv']]));
|
||||
}
|
||||
|
||||
if (! $wasPublished && $workflow['status'] === 'published') {
|
||||
app(StoryPublicationService::class)->afterPersistence($story, 'published', false);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'story_id' => (int) $story->id,
|
||||
'status' => $story->status,
|
||||
'message' => 'Story updated.',
|
||||
'edit_url' => route('creator.stories.edit', ['story' => $story->id]),
|
||||
'preview_url' => route('creator.stories.preview', ['story' => $story->id]),
|
||||
'analytics_url' => route('creator.stories.analytics', ['story' => $story->id]),
|
||||
'public_url' => route('stories.show', ['slug' => $story->slug]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -631,6 +677,7 @@ class StoryController extends Controller
|
||||
'og_image' => $validated['og_image'] ?? ($validated['cover_image'] ?? null),
|
||||
]);
|
||||
} else {
|
||||
$wasPublished = $story->published_at !== null || $story->status === 'published';
|
||||
$nextContent = array_key_exists('content', $validated)
|
||||
? $this->normalizeStoryContent($validated['content'])
|
||||
: (string) $story->content;
|
||||
@@ -655,6 +702,14 @@ class StoryController extends Controller
|
||||
'scheduled_for' => ! empty($validated['scheduled_for']) ? now()->parse((string) $validated['scheduled_for']) : $story->scheduled_for,
|
||||
]);
|
||||
$story->save();
|
||||
|
||||
if (! $wasPublished && $story->status === 'published') {
|
||||
if ($story->published_at === null) {
|
||||
$story->forceFill(['published_at' => now()])->save();
|
||||
}
|
||||
|
||||
app(StoryPublicationService::class)->afterPersistence($story, 'published', false);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($validated['tags_csv'])) {
|
||||
@@ -666,6 +721,10 @@ class StoryController extends Controller
|
||||
'story_id' => (int) $story->id,
|
||||
'saved_at' => now()->toIso8601String(),
|
||||
'message' => 'Saved just now',
|
||||
'edit_url' => route('creator.stories.edit', ['story' => $story->id]),
|
||||
'preview_url' => route('creator.stories.preview', ['story' => $story->id]),
|
||||
'analytics_url' => route('creator.stories.analytics', ['story' => $story->id]),
|
||||
'public_url' => route('stories.show', ['slug' => $story->slug]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1047,7 +1106,7 @@ class StoryController extends Controller
|
||||
'orderedList' => '<ol>' . $inner . '</ol>',
|
||||
'listItem' => '<li>' . $inner . '</li>',
|
||||
'horizontalRule' => '<hr>',
|
||||
'codeBlock' => '<pre><code>' . e($this->extractTipTapText($node)) . '</code></pre>',
|
||||
'codeBlock' => $this->renderCodeBlockNode($attrs, $node),
|
||||
'image' => $this->renderImageNode($attrs),
|
||||
'artworkEmbed' => $this->renderArtworkEmbedNode($attrs),
|
||||
'galleryBlock' => $this->renderGalleryBlockNode($attrs),
|
||||
@@ -1057,6 +1116,23 @@ class StoryController extends Controller
|
||||
};
|
||||
}
|
||||
|
||||
private function renderCodeBlockNode(array $attrs, array $node): string
|
||||
{
|
||||
$language = strtolower(trim((string) ($attrs['language'] ?? '')));
|
||||
$language = preg_match('/^[a-z0-9_+-]+$/', $language) === 1 ? $language : '';
|
||||
$escapedCode = e($this->extractTipTapText($node));
|
||||
|
||||
$preAttributes = $language !== ''
|
||||
? ' data-language="' . e($language) . '"'
|
||||
: '';
|
||||
|
||||
$codeAttributes = $language !== ''
|
||||
? ' class="language-' . e($language) . '" data-language="' . e($language) . '"'
|
||||
: '';
|
||||
|
||||
return '<pre' . $preAttributes . '><code' . $codeAttributes . '>' . $escapedCode . '</code></pre>';
|
||||
}
|
||||
|
||||
private function renderImageNode(array $attrs): string
|
||||
{
|
||||
$src = (string) ($attrs['src'] ?? '');
|
||||
|
||||
Reference in New Issue
Block a user