новости
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\SaveNewsRequest;
|
||||
use App\Models\News;
|
||||
use App\Services\NewsService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NewsController extends Controller
|
||||
{
|
||||
public function index(NewsService $news): View
|
||||
{
|
||||
return view('admin.news.index', [
|
||||
'newsItems' => $news->listForAdmin(),
|
||||
'newsStats' => $news->stats(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(SaveNewsRequest $request, NewsService $news): RedirectResponse
|
||||
{
|
||||
$news->create($request->validated(), $request->user(), $request->file('image'));
|
||||
|
||||
return back()->with('status', 'Новость добавлена.');
|
||||
}
|
||||
|
||||
public function update(
|
||||
News $newsItem,
|
||||
SaveNewsRequest $request,
|
||||
NewsService $news,
|
||||
): RedirectResponse {
|
||||
$news->update($newsItem, $request->validated(), $request->file('image'));
|
||||
|
||||
return back()->with('status', 'Новость обновлена.');
|
||||
}
|
||||
|
||||
public function destroy(News $newsItem, NewsService $news): RedirectResponse
|
||||
{
|
||||
$news->delete($newsItem);
|
||||
|
||||
return back()->with('status', 'Новость удалена.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\News;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class LauncherNewsController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/launcher/news',
|
||||
summary: 'Список новостей лаунчера',
|
||||
description: 'Возвращает опубликованные новости для отображения в лаунчере. Отсортированы по sort_order и дате создания.',
|
||||
tags: ['Launcher'],
|
||||
security: [['bearerAuth' => []]],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Список новостей',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'title', type: 'string', example: 'Обновление 1.2'),
|
||||
new OA\Property(property: 'body', type: 'string', example: 'Описание обновления...'),
|
||||
new OA\Property(property: 'image_url', type: 'string', format: 'uri', nullable: true, example: 'https://storage.yandexcloud.net/warcraft-client/news/update-1.2.jpg'),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
description: 'Не авторизован',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Unauthenticated.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$news = News::published()
|
||||
->ordered()
|
||||
->get()
|
||||
->map(fn (News $item) => [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'body' => $item->body,
|
||||
'image_url' => $item->image_url,
|
||||
'created_at' => $item->created_at->toIso8601String(),
|
||||
]);
|
||||
|
||||
return response()->json(['data' => $news]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SaveNewsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_published' => $this->boolean('is_published'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:120'],
|
||||
'body' => ['required', 'string', 'max:20000'],
|
||||
'sort_order' => ['required', 'integer', 'min:0', 'max:9999'],
|
||||
'is_published' => ['nullable', 'boolean'],
|
||||
'image' => ['nullable', 'image', 'max:2048'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => 'Укажи заголовок новости.',
|
||||
'title.max' => 'Заголовок не должен быть длиннее 120 символов.',
|
||||
'body.required' => 'Заполни текст новости.',
|
||||
'body.max' => 'Текст новости слишком длинный.',
|
||||
'sort_order.required' => 'Укажи порядок вывода.',
|
||||
'sort_order.integer' => 'Порядок вывода должен быть числом.',
|
||||
'sort_order.min' => 'Порядок вывода не может быть отрицательным.',
|
||||
'sort_order.max' => 'Порядок вывода слишком большой.',
|
||||
'image.image' => 'Файл должен быть изображением.',
|
||||
'image.max' => 'Изображение не должно превышать 2 МБ.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class News extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'body',
|
||||
'image_path',
|
||||
'sort_order',
|
||||
'is_published',
|
||||
'created_by_account_id',
|
||||
'created_by_username',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_published' => 'bool',
|
||||
];
|
||||
}
|
||||
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->orderBy('sort_order')
|
||||
->orderByDesc('id');
|
||||
}
|
||||
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_published', true);
|
||||
}
|
||||
|
||||
public function getImageUrlAttribute(): ?string
|
||||
{
|
||||
if (! $this->image_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Storage::disk('public')->url($this->image_path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GameAccountUser;
|
||||
use App\Models\News;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class NewsService
|
||||
{
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, \App\Models\News>
|
||||
*/
|
||||
public function listForAdmin(): Collection
|
||||
{
|
||||
return News::query()->ordered()->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{total: int, published: int, draft: int}
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$total = News::query()->count();
|
||||
$published = News::query()->published()->count();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'published' => $published,
|
||||
'draft' => $total - $published,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{title: string, body: string, sort_order: int, is_published?: bool} $attributes
|
||||
*/
|
||||
public function create(array $attributes, ?GameAccountUser $actor, ?\Illuminate\Http\UploadedFile $image = null): News
|
||||
{
|
||||
$data = [
|
||||
...$this->normalizeAttributes($attributes),
|
||||
'created_by_account_id' => $actor?->id,
|
||||
'created_by_username' => $actor?->username,
|
||||
];
|
||||
|
||||
if ($image) {
|
||||
$data['image_path'] = $image->store('news', 'public');
|
||||
}
|
||||
|
||||
return News::query()->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{title: string, body: string, sort_order: int, is_published?: bool} $attributes
|
||||
*/
|
||||
public function update(News $news, array $attributes, ?\Illuminate\Http\UploadedFile $image = null): News
|
||||
{
|
||||
$data = $this->normalizeAttributes($attributes);
|
||||
|
||||
if ($image) {
|
||||
if ($news->image_path) {
|
||||
Storage::disk('public')->delete($news->image_path);
|
||||
}
|
||||
|
||||
$data['image_path'] = $image->store('news', 'public');
|
||||
}
|
||||
|
||||
$news->forceFill($data)->save();
|
||||
|
||||
return $news->refresh();
|
||||
}
|
||||
|
||||
public function delete(News $news): void
|
||||
{
|
||||
if ($news->image_path) {
|
||||
Storage::disk('public')->delete($news->image_path);
|
||||
}
|
||||
|
||||
$news->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{title: string, body: string, sort_order: int, is_published?: bool} $attributes
|
||||
* @return array{title: string, body: string, sort_order: int, is_published: bool}
|
||||
*/
|
||||
private function normalizeAttributes(array $attributes): array
|
||||
{
|
||||
return [
|
||||
'title' => trim($attributes['title']),
|
||||
'body' => trim($attributes['body']),
|
||||
'sort_order' => (int) $attributes['sort_order'],
|
||||
'is_published' => (bool) ($attributes['is_published'] ?? false),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user