Manage clients, testimonials and talks through the API
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
90
app/Http/Controllers/Api/V1/ClientController.php
Normal file
90
app/Http/Controllers/Api/V1/ClientController.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V1\ClientRequest;
|
||||
use App\Http\Resources\ClientResource;
|
||||
use App\Models\Client;
|
||||
use App\Support\TranslatablePayload;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
/**
|
||||
* The organisations the work was for, as the panel edits them.
|
||||
*/
|
||||
class ClientController extends Controller
|
||||
{
|
||||
protected const WITH = ['logoMedia'];
|
||||
|
||||
/**
|
||||
* Filters: is_public, search (name), trashed.
|
||||
*/
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$this->authorize('viewAny', Client::class);
|
||||
|
||||
$clients = Client::query()
|
||||
->with(self::WITH)
|
||||
->withCount('projects')
|
||||
->when($request->has('is_public'), fn ($q) => $q->where('is_public', $request->boolean('is_public')))
|
||||
->when($request->filled('search'), fn ($q) => $q->where('name', 'like', '%'.$request->string('search').'%'))
|
||||
->when($request->boolean('trashed'), fn ($q) => $q->onlyTrashed())
|
||||
->orderBy('position')
|
||||
->orderBy('name')
|
||||
->paginate(perPage: min($request->integer('per_page', 25), 100))
|
||||
->withQueryString();
|
||||
|
||||
return ClientResource::collection($clients);
|
||||
}
|
||||
|
||||
public function store(ClientRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('create', Client::class);
|
||||
|
||||
return ClientResource::make($this->save(new Client, $request))->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Client $client): ClientResource
|
||||
{
|
||||
$this->authorize('view', $client);
|
||||
|
||||
return ClientResource::make($client->load(self::WITH)->loadCount('projects'));
|
||||
}
|
||||
|
||||
public function update(ClientRequest $request, Client $client): ClientResource
|
||||
{
|
||||
$this->authorize('update', $client);
|
||||
|
||||
return ClientResource::make($this->save($client, $request));
|
||||
}
|
||||
|
||||
public function destroy(Client $client): JsonResponse
|
||||
{
|
||||
$this->authorize('delete', $client);
|
||||
|
||||
$client->delete();
|
||||
|
||||
return response()->json(status: 204);
|
||||
}
|
||||
|
||||
public function restore(Client $client): ClientResource
|
||||
{
|
||||
$this->authorize('restore', $client);
|
||||
|
||||
$client->restore();
|
||||
|
||||
return ClientResource::make($client->load(self::WITH)->loadCount('projects'));
|
||||
}
|
||||
|
||||
protected function save(Client $client, ClientRequest $request): Client
|
||||
{
|
||||
$client->fill(TranslatablePayload::merge($client, $request->validated(), $client->translatable));
|
||||
$client->save();
|
||||
|
||||
return $client->load(self::WITH)->loadCount('projects');
|
||||
}
|
||||
}
|
||||
81
app/Http/Controllers/Api/V1/TalkController.php
Normal file
81
app/Http/Controllers/Api/V1/TalkController.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V1\TalkRequest;
|
||||
use App\Http\Resources\TalkResource;
|
||||
use App\Models\Talk;
|
||||
use App\Support\TranslatablePayload;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
/**
|
||||
* Talks given elsewhere. Deleted for good: there is no bin for them.
|
||||
*/
|
||||
class TalkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Filters: is_public, member_id, project_id, search (title or event).
|
||||
*/
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$this->authorize('viewAny', Talk::class);
|
||||
|
||||
$talks = Talk::query()
|
||||
->when($request->has('is_public'), fn ($q) => $q->where('is_public', $request->boolean('is_public')))
|
||||
->when($request->filled('member_id'), fn ($q) => $q->where('member_id', $request->integer('member_id')))
|
||||
->when($request->filled('project_id'), fn ($q) => $q->where('project_id', $request->integer('project_id')))
|
||||
->when($request->filled('search'), function ($q) use ($request): void {
|
||||
$term = '%'.$request->string('search').'%';
|
||||
|
||||
$q->where(fn ($q) => $q->where('title', 'like', $term)->orWhere('event', 'like', $term));
|
||||
})
|
||||
->recentFirst()
|
||||
->paginate(perPage: min($request->integer('per_page', 25), 100))
|
||||
->withQueryString();
|
||||
|
||||
return TalkResource::collection($talks);
|
||||
}
|
||||
|
||||
public function store(TalkRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('create', Talk::class);
|
||||
|
||||
return TalkResource::make($this->save(new Talk, $request))->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Talk $talk): TalkResource
|
||||
{
|
||||
$this->authorize('view', $talk);
|
||||
|
||||
return TalkResource::make($talk);
|
||||
}
|
||||
|
||||
public function update(TalkRequest $request, Talk $talk): TalkResource
|
||||
{
|
||||
$this->authorize('update', $talk);
|
||||
|
||||
return TalkResource::make($this->save($talk, $request));
|
||||
}
|
||||
|
||||
public function destroy(Talk $talk): JsonResponse
|
||||
{
|
||||
$this->authorize('delete', $talk);
|
||||
|
||||
$talk->delete();
|
||||
|
||||
return response()->json(status: 204);
|
||||
}
|
||||
|
||||
protected function save(Talk $talk, TalkRequest $request): Talk
|
||||
{
|
||||
$talk->fill(TranslatablePayload::merge($talk, $request->validated(), $talk->translatable));
|
||||
$talk->save();
|
||||
|
||||
return $talk;
|
||||
}
|
||||
}
|
||||
92
app/Http/Controllers/Api/V1/TestimonialController.php
Normal file
92
app/Http/Controllers/Api/V1/TestimonialController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V1\TestimonialRequest;
|
||||
use App\Http\Resources\TestimonialResource;
|
||||
use App\Models\Testimonial;
|
||||
use App\Support\TranslatablePayload;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
/**
|
||||
* What people the collective worked with said about it.
|
||||
*/
|
||||
class TestimonialController extends Controller
|
||||
{
|
||||
protected const WITH = ['avatarMedia'];
|
||||
|
||||
/**
|
||||
* Filters: is_public, client_id, project_id, service_id, search (author), trashed.
|
||||
*/
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$this->authorize('viewAny', Testimonial::class);
|
||||
|
||||
$testimonials = Testimonial::query()
|
||||
->with(self::WITH)
|
||||
->when($request->has('is_public'), fn ($q) => $q->where('is_public', $request->boolean('is_public')))
|
||||
->when($request->filled('client_id'), fn ($q) => $q->where('client_id', $request->integer('client_id')))
|
||||
->when($request->filled('project_id'), fn ($q) => $q->where('project_id', $request->integer('project_id')))
|
||||
->when($request->filled('service_id'), fn ($q) => $q->where('service_id', $request->integer('service_id')))
|
||||
->when($request->filled('search'), fn ($q) => $q->where('author_name', 'like', '%'.$request->string('search').'%'))
|
||||
->when($request->boolean('trashed'), fn ($q) => $q->onlyTrashed())
|
||||
->orderBy('position')
|
||||
->latest('id')
|
||||
->paginate(perPage: min($request->integer('per_page', 25), 100))
|
||||
->withQueryString();
|
||||
|
||||
return TestimonialResource::collection($testimonials);
|
||||
}
|
||||
|
||||
public function store(TestimonialRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('create', Testimonial::class);
|
||||
|
||||
return TestimonialResource::make($this->save(new Testimonial, $request))->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Testimonial $testimonial): TestimonialResource
|
||||
{
|
||||
$this->authorize('view', $testimonial);
|
||||
|
||||
return TestimonialResource::make($testimonial->load(self::WITH));
|
||||
}
|
||||
|
||||
public function update(TestimonialRequest $request, Testimonial $testimonial): TestimonialResource
|
||||
{
|
||||
$this->authorize('update', $testimonial);
|
||||
|
||||
return TestimonialResource::make($this->save($testimonial, $request));
|
||||
}
|
||||
|
||||
public function destroy(Testimonial $testimonial): JsonResponse
|
||||
{
|
||||
$this->authorize('delete', $testimonial);
|
||||
|
||||
$testimonial->delete();
|
||||
|
||||
return response()->json(status: 204);
|
||||
}
|
||||
|
||||
public function restore(Testimonial $testimonial): TestimonialResource
|
||||
{
|
||||
$this->authorize('restore', $testimonial);
|
||||
|
||||
$testimonial->restore();
|
||||
|
||||
return TestimonialResource::make($testimonial->load(self::WITH));
|
||||
}
|
||||
|
||||
protected function save(Testimonial $testimonial, TestimonialRequest $request): Testimonial
|
||||
{
|
||||
$testimonial->fill(TranslatablePayload::merge($testimonial, $request->validated(), $testimonial->translatable));
|
||||
$testimonial->save();
|
||||
|
||||
return $testimonial->load(self::WITH);
|
||||
}
|
||||
}
|
||||
47
app/Http/Requests/Api/V1/ClientRequest.php
Normal file
47
app/Http/Requests/Api/V1/ClientRequest.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Rules\Translatable;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* The panel's client form, as rules.
|
||||
*/
|
||||
class ClientRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$client = $this->route('client');
|
||||
$creating = ! $client instanceof Client;
|
||||
$sometimes = $creating ? 'required' : 'sometimes';
|
||||
|
||||
return [
|
||||
'name' => [$sometimes, 'string', 'max:255'],
|
||||
'slug' => [
|
||||
$sometimes,
|
||||
'string',
|
||||
'max:255',
|
||||
'alpha_dash:ascii',
|
||||
Rule::unique('clients', 'slug')->ignore($client?->id),
|
||||
],
|
||||
'sector' => ['sometimes', 'nullable', new Translatable(max: 120)],
|
||||
'logo' => ['sometimes', 'nullable', 'string', 'max:2048'],
|
||||
'url' => ['sometimes', 'nullable', 'url', 'max:2048'],
|
||||
'is_public' => ['sometimes', 'boolean'],
|
||||
'position' => ['sometimes', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
45
app/Http/Requests/Api/V1/TalkRequest.php
Normal file
45
app/Http/Requests/Api/V1/TalkRequest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use App\Models\Talk;
|
||||
use App\Rules\Translatable;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* The panel's talk form, as rules. The title is the one the talk was given
|
||||
* under, in `language`; only the summary is translated.
|
||||
*/
|
||||
class TalkRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$creating = ! $this->route('talk') instanceof Talk;
|
||||
$sometimes = $creating ? 'required' : 'sometimes';
|
||||
|
||||
return [
|
||||
'title' => [$sometimes, 'string', 'max:255'],
|
||||
'language' => [$sometimes, 'string', Rule::in(array_keys((array) config('locales.supported')))],
|
||||
'summary' => ['sometimes', 'nullable', new Translatable(max: 500)],
|
||||
'event' => [$sometimes, 'string', 'max:255'],
|
||||
'city' => ['sometimes', 'nullable', 'string', 'max:120'],
|
||||
'held_at' => ['sometimes', 'nullable', 'date'],
|
||||
'video_url' => ['sometimes', 'nullable', 'url', 'max:2048'],
|
||||
'slides_url' => ['sometimes', 'nullable', 'url', 'max:2048'],
|
||||
'member_id' => ['sometimes', 'nullable', 'integer', Rule::exists('members', 'id')->whereNull('deleted_at')],
|
||||
'project_id' => ['sometimes', 'nullable', 'integer', Rule::exists('projects', 'id')->whereNull('deleted_at')],
|
||||
'is_public' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
47
app/Http/Requests/Api/V1/TestimonialRequest.php
Normal file
47
app/Http/Requests/Api/V1/TestimonialRequest.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use App\Models\Testimonial;
|
||||
use App\Rules\Translatable;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* The panel's testimonial form, as rules.
|
||||
*
|
||||
* `original_locale` says which language the person actually spoke in; the
|
||||
* other entries of `quote` are translations of it.
|
||||
*/
|
||||
class TestimonialRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$creating = ! $this->route('testimonial') instanceof Testimonial;
|
||||
$sometimes = $creating ? 'required' : 'sometimes';
|
||||
|
||||
return [
|
||||
'quote' => [$sometimes, new Translatable(max: 1000)],
|
||||
'original_locale' => [$sometimes, 'string', Rule::in(array_keys((array) config('locales.supported')))],
|
||||
'author_name' => [$sometimes, 'string', 'max:255'],
|
||||
'author_role' => ['sometimes', 'nullable', new Translatable(max: 255)],
|
||||
'avatar' => ['sometimes', 'nullable', 'string', 'max:2048'],
|
||||
'client_id' => ['sometimes', 'nullable', 'integer', Rule::exists('clients', 'id')->whereNull('deleted_at')],
|
||||
'project_id' => ['sometimes', 'nullable', 'integer', Rule::exists('projects', 'id')->whereNull('deleted_at')],
|
||||
'service_id' => ['sometimes', 'nullable', 'integer', Rule::exists('services', 'id')->whereNull('deleted_at')],
|
||||
'is_public' => ['sometimes', 'boolean'],
|
||||
'position' => ['sometimes', 'integer', 'min:0'],
|
||||
'given_at' => ['sometimes', 'nullable', 'date'],
|
||||
];
|
||||
}
|
||||
}
|
||||
54
app/Http/Resources/ClientResource.php
Normal file
54
app/Http/Resources/ClientResource.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Support\ImagePayload;
|
||||
use App\Support\TranslatablePayload;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Client
|
||||
*/
|
||||
class ClientResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$locale = $request->query(config('locales.query_parameter')) ?? app()->getLocale();
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'locale' => $locale,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
|
||||
...TranslatablePayload::resolve($this->resource, $this->translatable, $locale),
|
||||
|
||||
'logo' => $this->logo,
|
||||
'logo_media' => ImagePayload::make(
|
||||
$this->logo,
|
||||
$this->resource->relationLoaded('logoMedia') ? $this->resource->getRelation('logoMedia') : null,
|
||||
$this->name,
|
||||
),
|
||||
'url' => $this->url,
|
||||
'is_public' => (bool) $this->is_public,
|
||||
'position' => $this->position,
|
||||
'projects_count' => $this->whenCounted('projects'),
|
||||
|
||||
'created_at' => $this->created_at?->toIso8601String(),
|
||||
'updated_at' => $this->updated_at?->toIso8601String(),
|
||||
'deleted_at' => $this->deleted_at?->toIso8601String(),
|
||||
|
||||
'translations' => $this->when(
|
||||
in_array('translations', explode(',', (string) $request->query('with')), true),
|
||||
fn () => TranslatablePayload::all($this->resource, $this->translatable),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
50
app/Http/Resources/TalkResource.php
Normal file
50
app/Http/Resources/TalkResource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\Talk;
|
||||
use App\Support\TranslatablePayload;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Talk
|
||||
*/
|
||||
class TalkResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$locale = $request->query(config('locales.query_parameter')) ?? app()->getLocale();
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'locale' => $locale,
|
||||
'title' => $this->title,
|
||||
'language' => $this->language,
|
||||
|
||||
...TranslatablePayload::resolve($this->resource, $this->translatable, $locale),
|
||||
|
||||
'event' => $this->event,
|
||||
'city' => $this->city,
|
||||
'held_at' => $this->held_at?->toDateString(),
|
||||
'video_url' => $this->video_url,
|
||||
'slides_url' => $this->slides_url,
|
||||
'member_id' => $this->member_id,
|
||||
'project_id' => $this->project_id,
|
||||
'is_public' => (bool) $this->is_public,
|
||||
|
||||
'created_at' => $this->created_at?->toIso8601String(),
|
||||
'updated_at' => $this->updated_at?->toIso8601String(),
|
||||
|
||||
'translations' => $this->when(
|
||||
in_array('translations', explode(',', (string) $request->query('with')), true),
|
||||
fn () => TranslatablePayload::all($this->resource, $this->translatable),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
56
app/Http/Resources/TestimonialResource.php
Normal file
56
app/Http/Resources/TestimonialResource.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\Testimonial;
|
||||
use App\Support\ImagePayload;
|
||||
use App\Support\TranslatablePayload;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Testimonial
|
||||
*/
|
||||
class TestimonialResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$locale = $request->query(config('locales.query_parameter')) ?? app()->getLocale();
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'locale' => $locale,
|
||||
|
||||
...TranslatablePayload::resolve($this->resource, $this->translatable, $locale),
|
||||
|
||||
'original_locale' => $this->original_locale,
|
||||
'author_name' => $this->author_name,
|
||||
'avatar' => $this->avatar,
|
||||
'avatar_media' => ImagePayload::make(
|
||||
$this->avatar,
|
||||
$this->resource->relationLoaded('avatarMedia') ? $this->resource->getRelation('avatarMedia') : null,
|
||||
$this->author_name,
|
||||
),
|
||||
'client_id' => $this->client_id,
|
||||
'project_id' => $this->project_id,
|
||||
'service_id' => $this->service_id,
|
||||
'is_public' => (bool) $this->is_public,
|
||||
'position' => $this->position,
|
||||
'given_at' => $this->given_at?->toDateString(),
|
||||
|
||||
'created_at' => $this->created_at?->toIso8601String(),
|
||||
'updated_at' => $this->updated_at?->toIso8601String(),
|
||||
'deleted_at' => $this->deleted_at?->toIso8601String(),
|
||||
|
||||
'translations' => $this->when(
|
||||
in_array('translations', explode(',', (string) $request->query('with')), true),
|
||||
fn () => TranslatablePayload::all($this->resource, $this->translatable),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user