withoutTrashed() ->withCount('posts') ->withMax('posts', 'created_at') ->with([ 'user' => fn ($query) => $query ->withCount(['posts', 'thanksGiven', 'thanksReceived']) ->with(['rank', 'roles']), 'latestPost.user.rank', 'latestPost.user.roles', ]); $forumParam = $request->query('forum'); if (is_string($forumParam)) { $forumId = $this->parseIriId($forumParam); if ($forumId !== null) { $query->where('forum_id', $forumId); } } $threads = $query ->orderByDesc(DB::raw('COALESCE(posts_max_created_at, threads.created_at)')) ->get() ->map(fn (Thread $thread) => $this->serializeThread($thread)); return response()->json($threads); } public function show(Thread $thread): JsonResponse { $thread->increment('views_count'); $thread->refresh(); $thread->loadMissing([ 'user' => fn ($query) => $query ->withCount(['posts', 'thanksGiven', 'thanksReceived']) ->with(['rank', 'roles']), 'latestPost.user.rank', 'latestPost.user.roles', ])->loadCount('posts'); return response()->json($this->serializeThread($thread)); } public function store(Request $request): JsonResponse { $data = $request->validate([ 'title' => ['required', 'string'], 'body' => ['required', 'string'], 'forum' => ['required', 'string'], ]); $forumId = $this->parseIriId($data['forum']); $forum = Forum::findOrFail($forumId); if ($forum->type !== 'forum') { return response()->json(['message' => 'Threads can only be created inside forums.'], 422); } $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => $request->user()?->id, 'title' => $data['title'], 'body' => $data['body'], ]); $thread->loadMissing([ 'user' => fn ($query) => $query ->withCount(['posts', 'thanksGiven', 'thanksReceived']) ->with(['rank', 'roles']), 'latestPost.user.rank', 'latestPost.user.roles', ])->loadCount('posts'); return response()->json($this->serializeThread($thread), 201); } public function destroy(Request $request, Thread $thread): JsonResponse { $thread->deleted_by = $request->user()?->id; $thread->save(); $thread->delete(); return response()->json(null, 204); } private function parseIriId(?string $value): ?int { if (!$value) { return null; } if (preg_match('#/forums/(\d+)$#', $value, $matches)) { return (int) $matches[1]; } if (is_numeric($value)) { return (int) $value; } return null; } private function serializeThread(Thread $thread): array { return [ 'id' => $thread->id, 'title' => $thread->title, 'body' => $thread->body, 'forum' => "/api/forums/{$thread->forum_id}", 'user_id' => $thread->user_id, 'posts_count' => $thread->posts_count ?? 0, 'views_count' => $thread->views_count ?? 0, 'user_name' => $thread->user?->name, 'user_avatar_url' => $thread->user?->avatar_path ? Storage::url($thread->user->avatar_path) : null, 'user_posts_count' => $thread->user?->posts_count, 'user_created_at' => $thread->user?->created_at?->toIso8601String(), 'user_location' => $thread->user?->location, 'user_thanks_given_count' => $thread->user?->thanks_given_count ?? 0, 'user_thanks_received_count' => $thread->user?->thanks_received_count ?? 0, 'user_rank_name' => $thread->user?->rank?->name, 'user_rank_badge_type' => $thread->user?->rank?->badge_type, 'user_rank_badge_text' => $thread->user?->rank?->badge_text, 'user_rank_badge_url' => $thread->user?->rank?->badge_image_path ? Storage::url($thread->user->rank->badge_image_path) : null, 'user_rank_color' => $thread->user?->rank?->color, 'user_group_color' => $this->resolveGroupColor($thread->user), 'last_post_at' => $thread->latestPost?->created_at?->toIso8601String() ?? $thread->created_at?->toIso8601String(), 'last_post_id' => $thread->latestPost?->id, 'last_post_user_id' => $thread->latestPost?->user_id ?? $thread->user_id, 'last_post_user_name' => $thread->latestPost?->user?->name ?? $thread->user?->name, 'last_post_user_rank_color' => $thread->latestPost?->user?->rank?->color ?? $thread->user?->rank?->color, 'last_post_user_group_color' => $this->resolveGroupColor($thread->latestPost?->user) ?? $this->resolveGroupColor($thread->user), 'created_at' => $thread->created_at?->toIso8601String(), 'updated_at' => $thread->updated_at?->toIso8601String(), ]; } private function resolveGroupColor(?\App\Models\User $user): ?string { if (!$user) { return null; } $roles = $user->roles; if (!$roles) { return null; } foreach ($roles->sortBy('name') as $role) { if (!empty($role->color)) { return $role->color; } } return null; } }