новости
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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('news', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('title', 120);
|
||||||
|
$table->text('body');
|
||||||
|
$table->string('image_path')->nullable();
|
||||||
|
$table->unsignedSmallInteger('sort_order')->default(0)->index();
|
||||||
|
$table->boolean('is_published')->default(true)->index();
|
||||||
|
$table->unsignedInteger('created_by_account_id')->nullable()->index();
|
||||||
|
$table->string('created_by_username', 32)->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('news');
|
||||||
|
}
|
||||||
|
};
|
||||||
+91
-5
@@ -197,8 +197,8 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Launcher"
|
"Launcher"
|
||||||
],
|
],
|
||||||
"summary": "Скачать файл клиента",
|
"summary": "Получить ссылку на скачивание файла",
|
||||||
"description": "Стримит файл из S3 по относительному пути из манифеста. Путь может содержать вложенные директории (например `Data/ruRU/patch-ruRU-3.MPQ`).",
|
"description": "Возвращает временную presigned-ссылку на файл в S3. Лаунчер скачивает файл напрямую из хранилища по этой ссылке. Путь может содержать вложенные директории (например `Data/ruRU/patch-ruRU-3.MPQ`).",
|
||||||
"operationId": "9a94331833a7af795c1c57dfa3a77eb3",
|
"operationId": "9a94331833a7af795c1c57dfa3a77eb3",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -214,12 +214,23 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Бинарное содержимое файла",
|
"description": "Временная ссылка на скачивание",
|
||||||
"content": {
|
"content": {
|
||||||
"application/octet-stream": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"description": "Presigned URL для скачивания",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "binary"
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"expires_in": {
|
||||||
|
"description": "Время жизни ссылки в секундах",
|
||||||
|
"type": "integer",
|
||||||
|
"example": 3600
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,6 +274,81 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/launcher/news": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Launcher"
|
||||||
|
],
|
||||||
|
"summary": "Список новостей лаунчера",
|
||||||
|
"description": "Возвращает опубликованные новости для отображения в лаунчере. Отсортированы по sort_order и дате создания.",
|
||||||
|
"operationId": "69aa09e0c7cca466e8a45a186444245e",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Список новостей",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Обновление 1.2"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Описание обновления..."
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://storage.yandexcloud.net/warcraft-client/news/update-1.2.jpg",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Не авторизован",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Unauthenticated."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
@extends('layouts.portal')
|
||||||
|
|
||||||
|
@section('portal-content')
|
||||||
|
<section class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<span class="eyebrow">Админка</span>
|
||||||
|
<h2>Новости</h2>
|
||||||
|
<p>Новости отображаются в лаунчере. Картинка загружается в S3-хранилище.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="portal-grid">
|
||||||
|
<article class="content-card">
|
||||||
|
<h3>Статистика</h3>
|
||||||
|
|
||||||
|
<dl class="stats-list stats-list--compact">
|
||||||
|
<div>
|
||||||
|
<dt>Всего</dt>
|
||||||
|
<dd>{{ $newsStats['total'] }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Опубликовано</dt>
|
||||||
|
<dd>{{ $newsStats['published'] }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Черновиков</dt>
|
||||||
|
<dd>{{ $newsStats['draft'] }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<article class="content-card">
|
||||||
|
<span class="eyebrow">Новая запись</span>
|
||||||
|
<h2>Добавить новость</h2>
|
||||||
|
|
||||||
|
<form class="stack-form" method="POST" action="{{ route('admin.news.store') }}" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Заголовок</span>
|
||||||
|
<input class="form-control @error('title') is-invalid @enderror" type="text" name="title" value="{{ old('title') }}" maxlength="120" required>
|
||||||
|
@error('title')<span class="form-error">{{ $message }}</span>@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Порядок вывода</span>
|
||||||
|
<input class="form-control @error('sort_order') is-invalid @enderror" type="number" name="sort_order" value="{{ old('sort_order', 0) }}" min="0" max="9999" required>
|
||||||
|
@error('sort_order')<span class="form-error">{{ $message }}</span>@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
<input type="checkbox" name="is_published" value="1" @checked(old('is_published', true))>
|
||||||
|
<span>Сразу опубликовать</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Текст новости</span>
|
||||||
|
<textarea class="form-control form-control--textarea @error('body') is-invalid @enderror" name="body" required>{{ old('body') }}</textarea>
|
||||||
|
@error('body')<span class="form-error">{{ $message }}</span>@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Картинка</span>
|
||||||
|
<input class="form-control @error('image') is-invalid @enderror" type="file" name="image" accept="image/*">
|
||||||
|
<small>Максимум 2 МБ. Необязательно.</small>
|
||||||
|
@error('image')<span class="form-error">{{ $message }}</span>@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="button button--gold" type="submit">Добавить новость</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<span class="eyebrow">Текущие записи</span>
|
||||||
|
<h2>Редактирование</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="account-list">
|
||||||
|
@forelse ($newsItems as $newsItem)
|
||||||
|
<article class="content-card news-card">
|
||||||
|
<div class="account-card__header">
|
||||||
|
<div>
|
||||||
|
<h3>{{ $newsItem->title }}</h3>
|
||||||
|
<p>
|
||||||
|
Порядок: {{ $newsItem->sort_order }}
|
||||||
|
· {{ $newsItem->created_by_username ?? '—' }}
|
||||||
|
@if ($newsItem->updated_at)
|
||||||
|
· {{ $newsItem->updated_at->format('d.m.Y H:i') }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="tag">{{ $newsItem->is_published ? 'Опубликовано' : 'Черновик' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($newsItem->image_url)
|
||||||
|
<img src="{{ $newsItem->image_url }}" alt="{{ $newsItem->title }}" style="max-width: 320px; border-radius: 6px; margin-bottom: 1rem;">
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form class="stack-form" method="POST" action="{{ route('admin.news.update', $newsItem) }}" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
@method('PATCH')
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Заголовок</span>
|
||||||
|
<input class="form-control" type="text" name="title" value="{{ $newsItem->title }}" maxlength="120" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Порядок вывода</span>
|
||||||
|
<input class="form-control" type="number" name="sort_order" value="{{ $newsItem->sort_order }}" min="0" max="9999" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
<input type="checkbox" name="is_published" value="1" @checked($newsItem->is_published)>
|
||||||
|
<span>Опубликовано</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Текст новости</span>
|
||||||
|
<textarea class="form-control form-control--textarea" name="body" required>{{ $newsItem->body }}</textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Заменить картинку</span>
|
||||||
|
<input class="form-control" type="file" name="image" accept="image/*">
|
||||||
|
<small>Оставь пустым, чтобы сохранить текущую.</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="button button--gold" type="submit">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.news.destroy', $newsItem) }}">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
|
||||||
|
<button class="button button--ghost" type="submit" onclick="return confirm('Удалить эту новость?')">Удалить</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
@empty
|
||||||
|
<article class="content-card empty-state">
|
||||||
|
<h3>Новостей пока нет</h3>
|
||||||
|
<p>Добавь первую новость — она сразу появится в лаунчере.</p>
|
||||||
|
</article>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endsection
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Api\LauncherAuthController;
|
use App\Http\Controllers\Api\LauncherAuthController;
|
||||||
use App\Http\Controllers\Api\LauncherController;
|
use App\Http\Controllers\Api\LauncherController;
|
||||||
|
use App\Http\Controllers\Api\LauncherNewsController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::post('launcher/login', [LauncherAuthController::class, 'login'])
|
Route::post('launcher/login', [LauncherAuthController::class, 'login'])
|
||||||
@@ -11,4 +12,5 @@ Route::middleware('auth:api')->prefix('launcher')->group(function () {
|
|||||||
Route::get('manifest', [LauncherController::class, 'manifest']);
|
Route::get('manifest', [LauncherController::class, 'manifest']);
|
||||||
Route::get('download/{path}', [LauncherController::class, 'download'])
|
Route::get('download/{path}', [LauncherController::class, 'download'])
|
||||||
->where('path', '.*');
|
->where('path', '.*');
|
||||||
|
Route::get('news', [LauncherNewsController::class, 'index']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use App\Http\Controllers\Admin\AccountController;
|
use App\Http\Controllers\Admin\AccountController;
|
||||||
use App\Http\Controllers\Admin\InviteCodeController;
|
use App\Http\Controllers\Admin\InviteCodeController;
|
||||||
use App\Http\Controllers\Admin\LoginScreenNewsController;
|
use App\Http\Controllers\Admin\LoginScreenNewsController;
|
||||||
|
use App\Http\Controllers\Admin\NewsController;
|
||||||
use App\Http\Controllers\Auth\LoginController;
|
use App\Http\Controllers\Auth\LoginController;
|
||||||
use App\Http\Controllers\CabinetController;
|
use App\Http\Controllers\CabinetController;
|
||||||
use App\Http\Controllers\LandingController;
|
use App\Http\Controllers\LandingController;
|
||||||
@@ -40,4 +41,9 @@ Route::middleware(['auth', 'gamemaster'])
|
|||||||
Route::post('/login-screen-news', [LoginScreenNewsController::class, 'store'])->name('login-screen-news.store');
|
Route::post('/login-screen-news', [LoginScreenNewsController::class, 'store'])->name('login-screen-news.store');
|
||||||
Route::patch('/login-screen-news/{newsItem}', [LoginScreenNewsController::class, 'update'])->name('login-screen-news.update');
|
Route::patch('/login-screen-news/{newsItem}', [LoginScreenNewsController::class, 'update'])->name('login-screen-news.update');
|
||||||
Route::delete('/login-screen-news/{newsItem}', [LoginScreenNewsController::class, 'destroy'])->name('login-screen-news.destroy');
|
Route::delete('/login-screen-news/{newsItem}', [LoginScreenNewsController::class, 'destroy'])->name('login-screen-news.destroy');
|
||||||
|
|
||||||
|
Route::get('/news', [NewsController::class, 'index'])->name('news.index');
|
||||||
|
Route::post('/news', [NewsController::class, 'store'])->name('news.store');
|
||||||
|
Route::patch('/news/{newsItem}', [NewsController::class, 'update'])->name('news.update');
|
||||||
|
Route::delete('/news/{newsItem}', [NewsController::class, 'destroy'])->name('news.destroy');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -214,6 +214,9 @@ main() {
|
|||||||
log "Сгенерированы ключи Passport"
|
log "Сгенерированы ключи Passport"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
chmod 600 "${ROOT_DIR}/storage/oauth-private.key" "${ROOT_DIR}/storage/oauth-public.key"
|
||||||
|
log "Установлены права 600 на ключи Passport"
|
||||||
|
|
||||||
if ! app_exec php artisan passport:client --personal --no-interaction 2>/dev/null | grep -q 'already exists'; then
|
if ! app_exec php artisan passport:client --personal --no-interaction 2>/dev/null | grep -q 'already exists'; then
|
||||||
app_exec php artisan passport:client --personal --name="Launcher" --no-interaction
|
app_exec php artisan passport:client --personal --name="Launcher" --no-interaction
|
||||||
log "Создан Personal Access Client «Launcher»"
|
log "Создан Personal Access Client «Launcher»"
|
||||||
|
|||||||
Reference in New Issue
Block a user