Initial commit

This commit is contained in:
Zdeněk Burda
2026-01-09 21:26:40 +01:00
parent e83aec6dca
commit 41e3ce6f25
404 changed files with 61250 additions and 28 deletions

View File

@@ -0,0 +1,309 @@
<?php
namespace App\Http\Controllers;
use App\Models\NewsPost;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class NewsPostController extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
public function __construct()
{
// zápis jen pro přihlášené (admin policy vyřešíš přes Policy)
$this->middleware('auth:sanctum')->only(['store', 'update', 'destroy']);
}
/**
* Display a listing of the resource.
*
* Podporuje volitelný dotazový parametr ?lang=cs / ?lang=en
* Pokud je lang zadán, title/content/excerpt budou vráceny jen v daném jazyce.
*/
public function index(Request $request): JsonResponse
{
$perPage = (int) $request->get('per_page', 10);
$limit = (int) $request->get('limit', 0);
$includeUnpublished = $request->boolean('include_unpublished', false);
// volitelný jazyk pokud není, použije se app locale
$lang = $request->query('lang');
if (! is_string($lang) || $lang === '') {
$lang = app()->getLocale();
}
$query = NewsPost::query()
->orderByDesc('published_at');
if (! $includeUnpublished) {
$query->where('is_published', true)
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
if ($limit > 0) {
$items = $query->limit($limit)->get();
} else {
$items = $query->paginate($perPage);
}
$mapTranslation = function (NewsPost $post) use ($lang) {
$data = $post->toArray();
// getTranslation(attr, lang, useFallback=true)
$data['title'] = $post->getTranslation('title', $lang, true);
$data['content'] = $post->getTranslation('content', $lang, true);
$data['excerpt'] = $post->getTranslation('excerpt', $lang, true);
return $data;
};
if ($limit > 0) {
$items = $items->map($mapTranslation);
return response()->json($items);
}
$items->getCollection()->transform($mapTranslation);
return response()->json($items);
}
/**
* Detail novinky (přes slug).
* Public ale jen pokud je publikovaná, jinak 404.
*/
public function show(NewsPost $news): JsonResponse
{
if (
! $news->is_published ||
! $news->published_at ||
$news->published_at->isFuture()
) {
abort(404);
}
return response()->json($news);
}
/**
* Vytvoření novinky (admin).
*/
public function store(Request $request): JsonResponse
{
$this->authorize('create', NewsPost::class);
$data = $this->validateData($request);
if (empty($data['slug'])) {
$data['slug'] = $this->makeSlugFromTitle($data['title'] ?? null);
}
if (! empty($data['is_published']) && empty($data['published_at'])) {
$data['published_at'] = now();
}
$data['author_id'] = $request->user()?->id;
$news = NewsPost::create($data);
return response()->json($news, 201);
}
/**
* Aktualizace novinky (admin).
*/
public function update(Request $request, NewsPost $news): JsonResponse
{
$this->authorize('update', $news);
$data = $this->validateData($request, partial: true);
// pokud přišla změna title a není explicitně zadaný slug, dopočítej ho
if (
array_key_exists('title', $data) &&
(! array_key_exists('slug', $data) || empty($data['slug']))
) {
$generated = $this->makeSlugFromTitle($data['title']);
if ($generated !== null) {
$data['slug'] = $generated;
}
}
if (
array_key_exists('is_published', $data) &&
$data['is_published'] &&
empty($data['published_at'])
) {
$data['published_at'] = $news->published_at ?? now();
}
$news->fill($data);
$news->save();
return response()->json($news);
}
/**
* Smazání novinky (admin).
*/
public function destroy(NewsPost $news): JsonResponse
{
$this->authorize('delete', $news);
$news->delete();
return response()->json(null, 204);
}
/**
* Validace dat.
*
* Podporuje:
* - string hodnoty (jednotlivý překlad pro aktuální locale)
* - pole překladů: { "cs": "...", "en": "..." }
*/
protected function validateData(Request $request, bool $partial = false): array
{
$required = $partial ? 'sometimes' : 'required';
/** @var NewsPost|null $routeNews */
$routeNews = $request->route('news'); // může být null, nebo model díky route model bindingu
$rules = [
'title' => [
$required,
function (string $attribute, $value, \Closure $fail) {
if (is_string($value)) {
if (mb_strlen($value) > 255) {
$fail('The '.$attribute.' may not be greater than 255 characters.');
}
return;
}
if (is_array($value)) {
foreach ($value as $locale => $text) {
if (! is_string($text)) {
$fail("The {$attribute}.{$locale} must be a string.");
return;
}
if (mb_strlen($text) > 255) {
$fail("The {$attribute}.{$locale} may not be greater than 255 characters.");
return;
}
}
return;
}
$fail('The '.$attribute.' must be a string or an array of translated strings.');
},
],
'slug' => [
'sometimes',
'nullable',
'string',
'max:255',
Rule::unique('news_posts', 'slug')->ignore($routeNews?->getKey()),
],
'content' => [
$required,
function (string $attribute, $value, \Closure $fail) {
if (is_string($value)) {
return;
}
if (is_array($value)) {
foreach ($value as $locale => $text) {
if (! is_string($text)) {
$fail("The {$attribute}.{$locale} must be a string.");
return;
}
}
return;
}
$fail('The '.$attribute.' must be a string or an array of translated strings.');
},
],
'excerpt' => [
'sometimes',
function (string $attribute, $value, \Closure $fail) {
if ($value === null) {
return;
}
if (is_string($value)) {
if (mb_strlen($value) > 500) {
$fail('The '.$attribute.' may not be greater than 500 characters.');
}
return;
}
if (is_array($value)) {
foreach ($value as $locale => $text) {
if (! is_string($text)) {
$fail("The {$attribute}.{$locale} must be a string.");
return;
}
if (mb_strlen($text) > 500) {
$fail("The {$attribute}.{$locale} may not be greater than 500 characters.");
return;
}
}
return;
}
$fail('The '.$attribute.' must be null, a string, or an array of translated strings.');
},
],
'is_published' => ['sometimes', 'boolean'],
'published_at' => ['sometimes', 'nullable', 'date'],
];
return $request->validate($rules);
}
/**
* Vytvoří slug z titulku umí pracovat jak se stringem, tak s polem překladů.
*
* - pokud je $title string slug z něj
* - pokud je $title array použije se:
* title[aktuální_locale] || title['en'] || první dostupná hodnota
*/
protected function makeSlugFromTitle(string|array|null $title): ?string
{
if ($title === null) {
return null;
}
if (is_array($title)) {
$locale = app()->getLocale();
$base = $title[$locale]
?? $title['en']
?? reset($title);
if (! is_string($base) || $base === '') {
return null;
}
return Str::slug($base);
}
if ($title === '') {
return null;
}
return Str::slug($title);
}
}