Files
SkinbaseNova/app/Services/Posts/PostService.php
Gregor Klevze dc51d65440 feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile
Forum:
- TipTap WYSIWYG editor with full toolbar
- @emoji-mart/react emoji picker (consistent with tweets)
- @mention autocomplete with user search API
- Fix PHP 8.4 parse errors in Blade templates
- Fix thread data display (paginator items)
- Align forum page widths to max-w-5xl

Discover:
- Extract shared _nav.blade.php partial
- Add missing nav links to for-you page
- Add Following link for authenticated users

Feed/Posts:
- Post model, controllers, policies, migrations
- Feed page components (PostComposer, FeedCard, etc)
- Post reactions, comments, saves, reports, sharing
- Scheduled publishing support
- Link preview controller

Profile:
- Profile page components (ProfileHero, ProfileTabs)
- Profile API controller

Uploads:
- Upload wizard enhancements
- Scheduled publish picker
- Studio status bar and readiness checklist
2026-03-03 09:48:31 +01:00

116 lines
3.3 KiB
PHP

<?php
namespace App\Services\Posts;
use App\Models\Post;
use App\Models\PostTarget;
use App\Models\User;
use App\Services\ContentSanitizer;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class PostService
{
public function __construct(
private PostCountersService $counters,
private PostHashtagService $hashtags,
) {}
/**
* Create a new text post.
*
* @param User $user
* @param string $visibility
* @param string|null $body
* @param array $targets [['type'=>'artwork','id'=>123], ...]
* @return Post
*/
public function createPost(
User $user,
string $type,
string $visibility,
?string $body,
array $targets = [],
?array $linkPreview = null,
?array $taggedUsers = null,
?Carbon $publishAt = null,
): Post {
$sanitizedBody = $body ? ContentSanitizer::render($body) : null;
$status = ($publishAt && $publishAt->isFuture())
? Post::STATUS_SCHEDULED
: Post::STATUS_PUBLISHED;
$meta = [];
if ($linkPreview && ! empty($linkPreview['url'])) {
$meta['link_preview'] = array_intersect_key($linkPreview, array_flip(['url', 'title', 'description', 'image', 'site_name']));
}
if ($taggedUsers && count($taggedUsers) > 0) {
$meta['tagged_users'] = array_map(
fn ($u) => ['id' => (int) $u['id'], 'username' => (string) $u['username'], 'name' => (string) ($u['name'] ?? $u['username'])],
array_slice($taggedUsers, 0, 10),
);
}
return DB::transaction(function () use ($user, $type, $visibility, $sanitizedBody, $targets, $meta, $status, $publishAt) {
$post = Post::create([
'user_id' => $user->id,
'type' => $type,
'visibility' => $visibility,
'body' => $sanitizedBody,
'meta' => $meta ?: null,
'status' => $status,
'publish_at' => $publishAt,
]);
foreach ($targets as $target) {
PostTarget::create([
'post_id' => $post->id,
'target_type' => $target['type'],
'target_id' => $target['id'],
]);
}
// Sync hashtags extracted from the body
if ($sanitizedBody) {
$this->hashtags->sync($post, $sanitizedBody);
}
return $post;
});
}
/**
* Update body/visibility of an existing post.
*/
public function updatePost(Post $post, ?string $body, ?string $visibility): Post
{
$updates = [];
if ($body !== null) {
$updates['body'] = ContentSanitizer::render($body);
}
if ($visibility !== null) {
$updates['visibility'] = $visibility;
}
$post->update($updates);
// Re-sync hashtags whenever body changes
if (isset($updates['body'])) {
$this->hashtags->sync($post, $updates['body']);
}
return $post->fresh();
}
/**
* Soft-delete a post (cascades to targets/reactions/comments via DB).
*/
public function deletePost(Post $post): void
{
$post->delete();
}
}