189 lines
6.3 KiB
PHP
189 lines
6.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Story;
|
|
use App\Models\StoryTag;
|
|
use App\Models\StoryAuthor;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
/**
|
|
* Stories API — JSON endpoints for React frontend.
|
|
*
|
|
* GET /api/stories list published stories (paginated)
|
|
* GET /api/stories/{slug} single story detail
|
|
* GET /api/stories/tag/{tag} stories by tag
|
|
* GET /api/stories/author/{author} stories by author
|
|
* GET /api/stories/featured featured stories
|
|
*/
|
|
final class StoriesApiController extends Controller
|
|
{
|
|
/**
|
|
* List published stories (paginated).
|
|
* GET /api/stories?page=1&per_page=12
|
|
*/
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$perPage = min((int) $request->get('per_page', 12), 50);
|
|
$page = (int) $request->get('page', 1);
|
|
|
|
$cacheKey = "stories:api:list:{$perPage}:{$page}";
|
|
|
|
$stories = Cache::remember($cacheKey, 300, fn () =>
|
|
Story::published()
|
|
->with('author', 'tags')
|
|
->orderByDesc('published_at')
|
|
->paginate($perPage, ['*'], 'page', $page)
|
|
);
|
|
|
|
return response()->json([
|
|
'data' => $stories->getCollection()->map(fn (Story $s) => $this->formatCard($s)),
|
|
'meta' => [
|
|
'current_page' => $stories->currentPage(),
|
|
'last_page' => $stories->lastPage(),
|
|
'per_page' => $stories->perPage(),
|
|
'total' => $stories->total(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Single story detail.
|
|
* GET /api/stories/{slug}
|
|
*/
|
|
public function show(string $slug): JsonResponse
|
|
{
|
|
$story = Cache::remember('stories:api:' . $slug, 600, fn () =>
|
|
Story::published()
|
|
->with('author', 'tags')
|
|
->where('slug', $slug)
|
|
->firstOrFail()
|
|
);
|
|
|
|
return response()->json($this->formatFull($story));
|
|
}
|
|
|
|
/**
|
|
* Featured story.
|
|
* GET /api/stories/featured
|
|
*/
|
|
public function featured(): JsonResponse
|
|
{
|
|
$story = Cache::remember('stories:api:featured', 300, fn () =>
|
|
Story::published()->featured()
|
|
->with('author', 'tags')
|
|
->orderByDesc('published_at')
|
|
->first()
|
|
);
|
|
|
|
if (! $story) {
|
|
return response()->json(null);
|
|
}
|
|
|
|
return response()->json($this->formatFull($story));
|
|
}
|
|
|
|
/**
|
|
* Stories by tag.
|
|
* GET /api/stories/tag/{tag}?page=1
|
|
*/
|
|
public function byTag(Request $request, string $tag): JsonResponse
|
|
{
|
|
$storyTag = StoryTag::where('slug', $tag)->firstOrFail();
|
|
$page = (int) $request->get('page', 1);
|
|
|
|
$stories = Cache::remember("stories:api:tag:{$tag}:{$page}", 300, fn () =>
|
|
Story::published()
|
|
->with('author', 'tags')
|
|
->whereHas('tags', fn ($q) => $q->where('stories_tags.id', $storyTag->id))
|
|
->orderByDesc('published_at')
|
|
->paginate(12, ['*'], 'page', $page)
|
|
);
|
|
|
|
return response()->json([
|
|
'tag' => ['id' => $storyTag->id, 'slug' => $storyTag->slug, 'name' => $storyTag->name],
|
|
'data' => $stories->getCollection()->map(fn (Story $s) => $this->formatCard($s)),
|
|
'meta' => [
|
|
'current_page' => $stories->currentPage(),
|
|
'last_page' => $stories->lastPage(),
|
|
'per_page' => $stories->perPage(),
|
|
'total' => $stories->total(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Stories by author.
|
|
* GET /api/stories/author/{username}?page=1
|
|
*/
|
|
public function byAuthor(Request $request, string $username): JsonResponse
|
|
{
|
|
$author = StoryAuthor::whereHas('user', fn ($q) => $q->where('username', $username))->first()
|
|
?? StoryAuthor::where('name', $username)->firstOrFail();
|
|
|
|
$page = (int) $request->get('page', 1);
|
|
|
|
$stories = Cache::remember("stories:api:author:{$author->id}:{$page}", 300, fn () =>
|
|
Story::published()
|
|
->with('author', 'tags')
|
|
->where('author_id', $author->id)
|
|
->orderByDesc('published_at')
|
|
->paginate(12, ['*'], 'page', $page)
|
|
);
|
|
|
|
return response()->json([
|
|
'author' => $this->formatAuthor($author),
|
|
'data' => $stories->getCollection()->map(fn (Story $s) => $this->formatCard($s)),
|
|
'meta' => [
|
|
'current_page' => $stories->currentPage(),
|
|
'last_page' => $stories->lastPage(),
|
|
'per_page' => $stories->perPage(),
|
|
'total' => $stories->total(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
// ── Private formatters ────────────────────────────────────────────────
|
|
|
|
private function formatCard(Story $story): array
|
|
{
|
|
return [
|
|
'id' => $story->id,
|
|
'slug' => $story->slug,
|
|
'url' => $story->url,
|
|
'title' => $story->title,
|
|
'excerpt' => $story->excerpt,
|
|
'cover_image' => $story->cover_url,
|
|
'author' => $story->author ? $this->formatAuthor($story->author) : null,
|
|
'tags' => $story->tags->map(fn ($t) => ['id' => $t->id, 'slug' => $t->slug, 'name' => $t->name, 'url' => $t->url]),
|
|
'views' => $story->views,
|
|
'featured' => $story->featured,
|
|
'reading_time' => $story->reading_time,
|
|
'published_at' => $story->published_at?->toIso8601String(),
|
|
];
|
|
}
|
|
|
|
private function formatFull(Story $story): array
|
|
{
|
|
return array_merge($this->formatCard($story), [
|
|
'content' => $story->content,
|
|
]);
|
|
}
|
|
|
|
private function formatAuthor(StoryAuthor $author): array
|
|
{
|
|
return [
|
|
'id' => $author->id,
|
|
'name' => $author->name,
|
|
'avatar_url' => $author->avatar_url,
|
|
'bio' => $author->bio,
|
|
'profile_url' => $author->profile_url,
|
|
];
|
|
}
|
|
}
|