212 lines
7.6 KiB
PHP
212 lines
7.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Story;
|
|
use App\Models\StoryTag;
|
|
use App\Models\User;
|
|
use App\Notifications\StoryStatusNotification;
|
|
use App\Services\StoryPublicationService;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\View\View;
|
|
|
|
class StoryAdminController extends Controller
|
|
{
|
|
public function index(): View
|
|
{
|
|
$stories = Story::query()
|
|
->with(['creator'])
|
|
->latest('created_at')
|
|
->paginate(25);
|
|
|
|
return view('admin.stories.index', ['stories' => $stories]);
|
|
}
|
|
|
|
public function review(): View
|
|
{
|
|
$stories = Story::query()
|
|
->with(['creator'])
|
|
->where('status', 'pending_review')
|
|
->orderByDesc('submitted_for_review_at')
|
|
->paginate(25);
|
|
|
|
return view('admin.stories.review', ['stories' => $stories]);
|
|
}
|
|
|
|
public function create(): View
|
|
{
|
|
return view('admin.stories.create', [
|
|
'creators' => User::query()->orderBy('username')->limit(200)->get(['id', 'username']),
|
|
'tags' => StoryTag::query()->orderBy('name')->get(['id', 'name']),
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'creator_id' => ['required', 'integer', 'exists:users,id'],
|
|
'title' => ['required', 'string', 'max:255'],
|
|
'excerpt' => ['nullable', 'string', 'max:500'],
|
|
'cover_image' => ['nullable', 'string', 'max:500'],
|
|
'content' => ['required', 'string'],
|
|
'story_type' => ['required', 'in:creator_story,tutorial,interview,project_breakdown,announcement,resource'],
|
|
'status' => ['required', Rule::in(['draft', 'pending_review', 'published', 'scheduled', 'archived', 'rejected'])],
|
|
'tags' => ['nullable', 'array'],
|
|
'tags.*' => ['integer', 'exists:story_tags,id'],
|
|
]);
|
|
|
|
$story = Story::query()->create([
|
|
'creator_id' => (int) $validated['creator_id'],
|
|
'title' => $validated['title'],
|
|
'slug' => $this->uniqueSlug($validated['title']),
|
|
'excerpt' => $validated['excerpt'] ?? null,
|
|
'cover_image' => $validated['cover_image'] ?? null,
|
|
'content' => $validated['content'],
|
|
'story_type' => $validated['story_type'],
|
|
'reading_time' => max(1, (int) ceil(str_word_count(strip_tags((string) $validated['content'])) / 200)),
|
|
'status' => $validated['status'],
|
|
'published_at' => $validated['status'] === 'published' ? now() : null,
|
|
'submitted_for_review_at' => $validated['status'] === 'pending_review' ? now() : null,
|
|
]);
|
|
|
|
if (! empty($validated['tags'])) {
|
|
$story->tags()->sync($validated['tags']);
|
|
}
|
|
|
|
if ($validated['status'] === 'published') {
|
|
app(StoryPublicationService::class)->afterPersistence($story, 'published', false);
|
|
}
|
|
|
|
return redirect()->route('admin.stories.edit', ['story' => $story->id])
|
|
->with('status', 'Story created.');
|
|
}
|
|
|
|
public function edit(Story $story): View
|
|
{
|
|
$story->load('tags');
|
|
|
|
return view('admin.stories.edit', [
|
|
'story' => $story,
|
|
'creators' => User::query()->orderBy('username')->limit(200)->get(['id', 'username']),
|
|
'tags' => StoryTag::query()->orderBy('name')->get(['id', 'name']),
|
|
]);
|
|
}
|
|
|
|
public function update(Request $request, Story $story): RedirectResponse
|
|
{
|
|
$wasPublished = $story->published_at !== null || $story->status === 'published';
|
|
|
|
$validated = $request->validate([
|
|
'creator_id' => ['required', 'integer', 'exists:users,id'],
|
|
'title' => ['required', 'string', 'max:255'],
|
|
'excerpt' => ['nullable', 'string', 'max:500'],
|
|
'cover_image' => ['nullable', 'string', 'max:500'],
|
|
'content' => ['required', 'string'],
|
|
'story_type' => ['required', 'in:creator_story,tutorial,interview,project_breakdown,announcement,resource'],
|
|
'status' => ['required', Rule::in(['draft', 'pending_review', 'published', 'scheduled', 'archived', 'rejected'])],
|
|
'tags' => ['nullable', 'array'],
|
|
'tags.*' => ['integer', 'exists:story_tags,id'],
|
|
]);
|
|
|
|
$story->update([
|
|
'creator_id' => (int) $validated['creator_id'],
|
|
'title' => $validated['title'],
|
|
'excerpt' => $validated['excerpt'] ?? null,
|
|
'cover_image' => $validated['cover_image'] ?? null,
|
|
'content' => $validated['content'],
|
|
'story_type' => $validated['story_type'],
|
|
'reading_time' => max(1, (int) ceil(str_word_count(strip_tags((string) $validated['content'])) / 200)),
|
|
'status' => $validated['status'],
|
|
'published_at' => $validated['status'] === 'published' ? ($story->published_at ?? now()) : $story->published_at,
|
|
'submitted_for_review_at' => $validated['status'] === 'pending_review' ? ($story->submitted_for_review_at ?? now()) : $story->submitted_for_review_at,
|
|
]);
|
|
|
|
$story->tags()->sync($validated['tags'] ?? []);
|
|
|
|
if (! $wasPublished && $validated['status'] === 'published') {
|
|
app(StoryPublicationService::class)->afterPersistence($story, 'published', false);
|
|
}
|
|
|
|
return back()->with('status', 'Story updated.');
|
|
}
|
|
|
|
public function destroy(Story $story): RedirectResponse
|
|
{
|
|
$story->delete();
|
|
|
|
return redirect()->route('admin.stories.index')->with('status', 'Story deleted.');
|
|
}
|
|
|
|
public function publish(Story $story): RedirectResponse
|
|
{
|
|
app(StoryPublicationService::class)->publish($story, 'published', [
|
|
'published_at' => $story->published_at ?? now(),
|
|
'reviewed_at' => now(),
|
|
]);
|
|
|
|
return back()->with('status', 'Story published.');
|
|
}
|
|
|
|
public function show(Story $story): View
|
|
{
|
|
return view('admin.stories.show', [
|
|
'story' => $story->load(['creator', 'tags']),
|
|
]);
|
|
}
|
|
|
|
public function approve(Request $request, Story $story): RedirectResponse
|
|
{
|
|
app(StoryPublicationService::class)->publish($story, 'approved', [
|
|
'published_at' => $story->published_at ?? now(),
|
|
'reviewed_at' => now(),
|
|
'reviewed_by_id' => (int) $request->user()->id,
|
|
'rejected_reason' => null,
|
|
]);
|
|
|
|
return back()->with('status', 'Story approved and published.');
|
|
}
|
|
|
|
public function reject(Request $request, Story $story): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'reason' => ['required', 'string', 'max:1000'],
|
|
]);
|
|
|
|
$story->update([
|
|
'status' => 'rejected',
|
|
'reviewed_at' => now(),
|
|
'reviewed_by_id' => (int) $request->user()->id,
|
|
'rejected_reason' => $validated['reason'],
|
|
]);
|
|
|
|
$story->creator?->notify(new StoryStatusNotification($story, 'rejected', $validated['reason']));
|
|
|
|
return back()->with('status', 'Story rejected and creator notified.');
|
|
}
|
|
|
|
public function moderateComments(): View
|
|
{
|
|
return view('admin.stories.comments-moderation');
|
|
}
|
|
|
|
private function uniqueSlug(string $title): string
|
|
{
|
|
$base = Str::slug($title);
|
|
$slug = $base;
|
|
$n = 2;
|
|
|
|
while (Story::query()->where('slug', $slug)->exists()) {
|
|
$slug = $base . '-' . $n;
|
|
$n++;
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
}
|