Files
SkinbaseNova/app/Http/Controllers/Api/ArtworkCommentController.php
2026-02-26 21:12:32 +01:00

179 lines
7.8 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use App\Services\ContentSanitizer;
use App\Services\LegacySmileyMapper;
use App\Support\AvatarUrl;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Gate;
/**
* Artwork comment CRUD.
*
* POST /api/artworks/{artworkId}/comments → store
* PUT /api/artworks/{artworkId}/comments/{id} → update (own comment)
* DELETE /api/artworks/{artworkId}/comments/{id} → delete (own or admin)
* GET /api/artworks/{artworkId}/comments → list (paginated)
*/
class ArtworkCommentController extends Controller
{
private const MAX_LENGTH = 10_000;
// ─────────────────────────────────────────────────────────────────────────
// List
// ─────────────────────────────────────────────────────────────────────────
public function index(Request $request, int $artworkId): JsonResponse
{
$artwork = Artwork::public()->published()->findOrFail($artworkId);
$page = max(1, (int) $request->query('page', 1));
$perPage = 20;
$comments = ArtworkComment::with(['user', 'user.profile'])
->where('artwork_id', $artwork->id)
->where('is_approved', true)
->orderByDesc('created_at')
->paginate($perPage, ['*'], 'page', $page);
$userId = $request->user()?->id;
$items = $comments->getCollection()->map(fn ($c) => $this->formatComment($c, $userId));
return response()->json([
'data' => $items,
'meta' => [
'current_page' => $comments->currentPage(),
'last_page' => $comments->lastPage(),
'total' => $comments->total(),
'per_page' => $comments->perPage(),
],
]);
}
// ─────────────────────────────────────────────────────────────────────────
// Store
// ─────────────────────────────────────────────────────────────────────────
public function store(Request $request, int $artworkId): JsonResponse
{
$artwork = Artwork::public()->published()->findOrFail($artworkId);
$request->validate([
'content' => ['required', 'string', 'min:1', 'max:' . self::MAX_LENGTH],
]);
$raw = $request->input('content');
// Validate markdown-lite content
$errors = ContentSanitizer::validate($raw);
if ($errors) {
return response()->json(['errors' => ['content' => $errors]], 422);
}
$rendered = ContentSanitizer::render($raw);
$comment = ArtworkComment::create([
'artwork_id' => $artwork->id,
'user_id' => $request->user()->id,
'content' => $raw, // legacy column (plain text fallback)
'raw_content' => $raw,
'rendered_content' => $rendered,
'is_approved' => true, // auto-approve; extend with moderation as needed
]);
// Bust the comments cache for this user's 'all' feed
Cache::forget('comments.latest.all.page1');
$comment->load(['user', 'user.profile']);
return response()->json(['data' => $this->formatComment($comment, $request->user()->id)], 201);
}
// ─────────────────────────────────────────────────────────────────────────
// Update
// ─────────────────────────────────────────────────────────────────────────
public function update(Request $request, int $artworkId, int $commentId): JsonResponse
{
$comment = ArtworkComment::where('artwork_id', $artworkId)
->findOrFail($commentId);
Gate::authorize('update', $comment);
$request->validate([
'content' => ['required', 'string', 'min:1', 'max:' . self::MAX_LENGTH],
]);
$raw = $request->input('content');
$errors = ContentSanitizer::validate($raw);
if ($errors) {
return response()->json(['errors' => ['content' => $errors]], 422);
}
$rendered = ContentSanitizer::render($raw);
$comment->update([
'content' => $raw,
'raw_content' => $raw,
'rendered_content' => $rendered,
]);
Cache::forget('comments.latest.all.page1');
$comment->load(['user', 'user.profile']);
return response()->json(['data' => $this->formatComment($comment, $request->user()->id)]);
}
// ─────────────────────────────────────────────────────────────────────────
// Delete
// ─────────────────────────────────────────────────────────────────────────
public function destroy(Request $request, int $artworkId, int $commentId): JsonResponse
{
$comment = ArtworkComment::where('artwork_id', $artworkId)->findOrFail($commentId);
Gate::authorize('delete', $comment);
$comment->delete();
Cache::forget('comments.latest.all.page1');
return response()->json(['message' => 'Comment deleted.'], 200);
}
// ─────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────
private function formatComment(ArtworkComment $c, ?int $currentUserId): array
{
$user = $c->user;
$userId = (int) ($c->user_id ?? 0);
$avatarHash = $user?->profile?->avatar_hash ?? null;
return [
'id' => $c->id,
'raw_content' => $c->raw_content ?? $c->content,
'rendered_content' => $c->rendered_content ?? e(strip_tags($c->content ?? '')),
'created_at' => $c->created_at?->toIso8601String(),
'time_ago' => $c->created_at ? Carbon::parse($c->created_at)->diffForHumans() : null,
'can_edit' => $currentUserId === $userId,
'can_delete' => $currentUserId === $userId,
'user' => [
'id' => $userId,
'username' => $user?->username,
'display' => $user?->username ?? $user?->name ?? 'User',
'profile_url' => $user?->username ? '/@' . $user->username : '/profile/' . $userId,
'avatar_url' => AvatarUrl::forUser($userId, $avatarHash, 64),
],
];
}
}