Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
899 changes: 899 additions & 0 deletions .cursor/rules/laravel-blog-api-rules.mdc

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions app/Enums/ArticleStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,40 @@
enum ArticleStatus: string
{
case DRAFT = 'draft';
case REVIEW = 'review';
case SCHEDULED = 'scheduled';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
case TRASHED = 'trashed';

/**
* Get the display name for the status
*/
public function displayName(): string
{
return match ($this) {
self::DRAFT => 'Draft',
self::REVIEW => 'Under Review',
self::SCHEDULED => 'Scheduled',
self::PUBLISHED => 'Published',
self::ARCHIVED => 'Archived',
self::TRASHED => 'Trashed',
};
}

/**
* Check if the status is a published state
*/
public function isPublished(): bool
{
return in_array($this, [self::PUBLISHED, self::SCHEDULED]);
}

/**
* Check if the status is a draft state
*/
public function isDraft(): bool
{
return in_array($this, [self::DRAFT, self::REVIEW]);
}
}
42 changes: 42 additions & 0 deletions app/Enums/CommentStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum CommentStatus: string
{
case PENDING = 'pending';
case APPROVED = 'approved';
case REJECTED = 'rejected';
case SPAM = 'spam';

/**
* Get the display name for the status
*/
public function displayName(): string
{
return match ($this) {
self::PENDING => 'Pending',
self::APPROVED => 'Approved',
self::REJECTED => 'Rejected',
self::SPAM => 'Spam',
};
}

/**
* Check if the status is a published state
*/
public function isPublished(): bool
{
return $this === self::APPROVED;
}

/**
* Check if the status is a draft state
*/
public function isDraft(): bool
{
return in_array($this, [self::PENDING, self::REJECTED]);
}
}
24 changes: 19 additions & 5 deletions app/Enums/UserRole.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,23 @@

enum UserRole: string
{
case ADMINISTRATOR = 'Administrator';
case EDITOR = 'Editor';
case AUTHOR = 'Author';
case CONTRIBUTOR = 'Contributor';
case SUBSCRIBER = 'Subscriber';
case ADMINISTRATOR = 'administrator';
case EDITOR = 'editor';
case AUTHOR = 'author';
case CONTRIBUTOR = 'contributor';
case SUBSCRIBER = 'subscriber';

/**
* Get the display name for the role
*/
public function displayName(): string
{
return match ($this) {
self::ADMINISTRATOR => 'Administrator',
self::EDITOR => 'Editor',
self::AUTHOR => 'Author',
self::CONTRIBUTOR => 'Contributor',
self::SUBSCRIBER => 'Subscriber',
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1\Admin\Article;

use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Admin\Article\ApproveArticleRequest;
use App\Http\Resources\V1\Admin\Article\ArticleManagementResource;
use App\Services\ArticleManagementService;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

#[Group('Admin - Article Management', weight: 2)]
final class ApproveArticleController extends Controller
{
public function __construct(
private readonly ArticleManagementService $articleManagementService
) {}

/**
* Approve Article
*
* Approve an article and publish it
*
* @response array{status: true, message: string, data: ArticleManagementResource}
*/
public function __invoke(int $id, ApproveArticleRequest $request): JsonResponse
{
try {
$user = $request->user();
assert($user !== null);

$article = $this->articleManagementService->approveArticle($id, $user->id);

return response()->apiSuccess(
new ArticleManagementResource($article),
__('common.article_approved_successfully')
);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return response()->apiError(
__('common.article_not_found'),
Response::HTTP_NOT_FOUND
);
} catch (\Throwable $e) {
return response()->apiError(
__('common.something_went_wrong'),
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1\Admin\Article;

use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Admin\Article\FeatureArticleRequest;
use App\Http\Resources\V1\Admin\Article\ArticleManagementResource;
use App\Services\ArticleManagementService;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

#[Group('Admin - Article Management', weight: 2)]
final class FeatureArticleController extends Controller
{
public function __construct(
private readonly ArticleManagementService $articleManagementService
) {}

/**
* Feature Article
*
* Mark an article as featured
*
* @response array{status: true, message: string, data: ArticleManagementResource}
*/
public function __invoke(int $id, FeatureArticleRequest $request): JsonResponse
{
try {
$article = $this->articleManagementService->featureArticle($id);

return response()->apiSuccess(
new ArticleManagementResource($article),
__('common.article_featured_successfully')
);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return response()->apiError(
__('common.article_not_found'),
Response::HTTP_NOT_FOUND
);
} catch (\Throwable $e) {
return response()->apiError(
__('common.something_went_wrong'),
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1\Admin\Article;

use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Admin\Article\GetArticlesRequest;
use App\Http\Resources\MetaResource;
use App\Http\Resources\V1\Admin\Article\ArticleManagementResource;
use App\Services\ArticleManagementService;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

#[Group('Admin - Article Management', weight: 2)]
final class GetArticlesController extends Controller
{
public function __construct(
private readonly ArticleManagementService $articleManagementService
) {}

/**
* Get Articles List
*
* Retrieve a paginated list of articles with admin filters and management capabilities
*
* @response array{status: true, message: string, data: array{articles: ArticleManagementResource[], meta: MetaResource}}
*/
public function __invoke(GetArticlesRequest $request): JsonResponse
{
try {
$params = $request->withDefaults();
$articles = $this->articleManagementService->getArticles($params);
$articleCollection = ArticleManagementResource::collection($articles);
$articleCollectionData = $articleCollection->response()->getData(true);

if (! is_array($articleCollectionData) || ! isset($articleCollectionData['data'], $articleCollectionData['meta'])) {
throw new \RuntimeException(__('common.unexpected_response_format'));
}

return response()->apiSuccess(
[
'articles' => $articleCollectionData['data'],
'meta' => MetaResource::make($articleCollectionData['meta']),
],
__('common.success')
);
} catch (\Throwable $e) {
return response()->apiError(
__('common.something_went_wrong'),
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1\Admin\Article;

use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Admin\Article\ReportArticleRequest;
use App\Http\Resources\V1\Admin\Article\ArticleManagementResource;
use App\Services\ArticleManagementService;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

#[Group('Admin - Article Management', weight: 2)]
final class ReportArticleController extends Controller
{
public function __construct(
private readonly ArticleManagementService $articleManagementService
) {}

/**
* Report Article
*
* Report an article with a reason
*
* @response array{status: true, message: string, data: ArticleManagementResource}
*/
public function __invoke(int $id, ReportArticleRequest $request): JsonResponse
{
try {
$validated = $request->validated();
/** @var string $reason */
$reason = $validated['reason'] ?? 'No reason provided';
$article = $this->articleManagementService->reportArticle($id, $reason);

return response()->apiSuccess(
new ArticleManagementResource($article),
__('common.article_reported_successfully')
);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return response()->apiError(
__('common.article_not_found'),
Response::HTTP_NOT_FOUND
);
} catch (\Throwable $e) {
return response()->apiError(
__('common.something_went_wrong'),
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1\Admin\Article;

use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Admin\Article\ShowArticleRequest;
use App\Http\Resources\V1\Admin\Article\ArticleManagementResource;
use App\Models\Article;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

#[Group('Admin - Article Management', weight: 2)]
final class ShowArticleController extends Controller
{
/**
* Show Article
*
* Retrieve a single article with full admin management details
*
* @response array{status: true, message: string, data: ArticleManagementResource}
*/
public function __invoke(int $id, ShowArticleRequest $request): JsonResponse
{
try {
$article = Article::query()
->with(['author:id,name,email', 'categories:id,name,slug', 'tags:id,name,slug', 'comments.user:id,name,email'])
->withCount(['comments', 'authors'])
->findOrFail($id);

return response()->apiSuccess(
new ArticleManagementResource($article),
__('common.success')
);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return response()->apiError(
__('common.article_not_found'),
Response::HTTP_NOT_FOUND
);
} catch (\Throwable $e) {
return response()->apiError(
__('common.something_went_wrong'),
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Loading