diff --git a/app/Conductors/ArticleConductor.php b/app/Conductors/ArticleConductor.php index f088691..f97acb3 100644 --- a/app/Conductors/ArticleConductor.php +++ b/app/Conductors/ArticleConductor.php @@ -32,13 +32,14 @@ class ArticleConductor extends Conductor * * @var string[] */ - protected $includes = ['attachments', 'user']; + protected $includes = ['attachments', 'user', 'gallery']; /** * Run a scope query on the collection before anything else. * * @param Builder $builder The builder in use. + * @return void */ public function scope(Builder $builder): void { @@ -127,6 +128,19 @@ class ArticleConductor extends Conductor }); } + /** + * Include Gallery Field. + * + * @param Model $model Them model. + * @return mixed The model result. + */ + public function includeGallery(Model $model) + { + return $model->gallery()->get()->map(function ($item) { + return MediaConductor::includeModel(request(), 'gallery', $item->media); + }); + } + /** * Include User Field. * diff --git a/app/Conductors/MediaConductor.php b/app/Conductors/MediaConductor.php index 25fd9ee..9c1855a 100644 --- a/app/Conductors/MediaConductor.php +++ b/app/Conductors/MediaConductor.php @@ -2,9 +2,9 @@ namespace App\Conductors; +use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Auth\User; class MediaConductor extends Conductor { diff --git a/app/Conductors/UserConductor.php b/app/Conductors/UserConductor.php index a756a48..bfe67e5 100644 --- a/app/Conductors/UserConductor.php +++ b/app/Conductors/UserConductor.php @@ -39,14 +39,17 @@ class UserConductor extends Conductor { $user = auth()->user(); $data = $model->toArray(); + $limit = $this->fields($model); + + // echo 'USER--' . implode(',', $limit) . "\n"; if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) { - $fields = ['id', 'display_name']; - $data = arrayLimitKeys($data, $fields); + $limit = ['id', 'display_name']; } else { $data['permissions'] = $user->permissions; } + $data = arrayLimitKeys($data, $limit); return $data; } diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index 1c1678d..9f4c763 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -24,9 +24,13 @@ class ApiController extends Controller * @param array $data Response data. * @param integer $respondCode Response status code. * @param array $headers Response headers. + * @return JsonResponse */ - public function respondJson(array $data, int $respondCode = HttpResponseCodes::HTTP_OK, array $headers = []): JsonResponse - { + public function respondJson( + array $data, + int $respondCode = HttpResponseCodes::HTTP_OK, + array $headers = [] + ): JsonResponse { return response()->json($data, $respondCode, $headers); } @@ -34,9 +38,11 @@ class ApiController extends Controller * Return forbidden message * * @param string $message Response message. + * @return JsonResponse */ - public function respondForbidden(string $message = 'You do not have permission to access the resource.'): JsonResponse - { + public function respondForbidden( + string $message = 'You do not have permission to access the resource.' + ): JsonResponse { return response()->json(['message' => $message], HttpResponseCodes::HTTP_FORBIDDEN); } @@ -44,6 +50,7 @@ class ApiController extends Controller * Return forbidden message * * @param string $message Response message. + * @return JsonResponse */ public function respondNotFound(string $message = 'The resource was not found.'): JsonResponse { @@ -54,6 +61,7 @@ class ApiController extends Controller * Return too large message * * @param string $message Response message. + * @return JsonResponse */ public function respondTooLarge(string $message = 'The request entity is too large.'): JsonResponse { @@ -61,7 +69,9 @@ class ApiController extends Controller } /** - * Return no content + * Return no content. + * + * @return JsonResponse */ public function respondNoContent(): JsonResponse { @@ -69,7 +79,19 @@ class ApiController extends Controller } /** - * Return created + * Return no content + * + * @return JsonResponse + */ + public function respondNotImplmeneted(): JsonResponse + { + return response()->json([], HttpResponseCodes::HTTP_NOT_IMPLEMENTED); + } + + /** + * Return created. + * + * @return JsonResponse */ public function respondCreated(): JsonResponse { @@ -77,7 +99,9 @@ class ApiController extends Controller } /** - * Return accepted + * Return accepted. + * + * @return JsonResponse */ public function respondAccepted(): JsonResponse { @@ -89,9 +113,12 @@ class ApiController extends Controller * * @param string $message Error message. * @param integer $responseCode Resource code. + * @return JsonResponse */ - public function respondError(string $message, int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY): JsonResponse - { + public function respondError( + string $message, + int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY + ): JsonResponse { return response()->json([ 'message' => $message ], $responseCode); @@ -102,9 +129,12 @@ class ApiController extends Controller * * @param array $errors Error messages. * @param integer $responseCode Resource code. + * @return JsonResponse */ - public function respondWithErrors(array $errors, int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY): JsonResponse - { + public function respondWithErrors( + array $errors, + int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY + ): JsonResponse { $keys = array_keys($errors); $error = $errors[$keys[0]]; @@ -122,17 +152,20 @@ class ApiController extends Controller /** * Return resource data * - * @param array|Model|Collection $data Resource data. - * @param array $options Respond options. + * @param array|Model|Collection $data Resource data. + * @param array $options Respond options. + * @param callable|null $validationFn Optional validation function to check the data before responding. + * @return JsonResponse */ protected function respondAsResource( mixed $data, array $options = [], $validationFn = null ): JsonResponse { - $isCollection = $options['isCollection'] ?? false; - $appendData = $options['appendData'] ?? null; - $resourceName = $options['resourceName'] ?? null; + $isCollection = ($options['isCollection'] ?? false); + $appendData = ($options['appendData'] ?? null); + $resourceName = ($options['resourceName'] ?? ''); + $transformResourceName = ($options['transformResourceName'] ?? true); $respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK); if ($data === null || ($data instanceof Collection && $data->count() === 0)) { @@ -146,11 +179,11 @@ class ApiController extends Controller } } - if (is_null($resourceName) === true || empty($resourceName) === true) { + if (empty($resourceName) === true) { $resourceName = $this->resourceName; } - if (is_null($resourceName) === true || empty($resourceName) === true) { + if (empty($resourceName) === true) { $resourceName = get_class($this); $resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1)); $resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller')); @@ -163,15 +196,14 @@ class ApiController extends Controller } elseif (is_array($data) === true) { $dataArray = $data; } elseif ($data instanceof Model) { - $is_multiple = false; $dataArray = $data->toArray(); } $resource = []; if ($isCollection === true) { - $resource = [Str::plural($resourceName) => $dataArray]; + $resource = [$transformResourceName === true ? Str::plural($resourceName) : $resourceName => $dataArray]; } else { - $resource = [Str::singular($resourceName) => $dataArray]; + $resource = [$transformResourceName === true ? Str::singular($resourceName) : $resourceName => $dataArray]; } if ($appendData !== null) { @@ -180,4 +212,22 @@ class ApiController extends Controller return response()->json($resource, $respondCode); } + + /** + * Get the Controller Model Class name. + * + * @return string + */ + public function getModelClass(): string + { + $controllerClass = static::class; + + $modelName = 'App\\Models\\' . Str::replaceLast('Controller', '', Str::afterLast($controllerClass, '\\')); + + if (class_exists($modelName) === false) { + return $modelName; + } + + return $modelName; + } } diff --git a/app/Http/Controllers/Api/ArticleController.php b/app/Http/Controllers/Api/ArticleController.php index f612f4c..d334c2d 100644 --- a/app/Http/Controllers/Api/ArticleController.php +++ b/app/Http/Controllers/Api/ArticleController.php @@ -8,6 +8,8 @@ use App\Enum\HttpResponseCodes; use App\Http\Requests\ArticleRequest; use App\Models\Media; use App\Models\Article; +use App\Models\Gallery; +use App\Traits\HasGallery; use Illuminate\Http\JsonResponse; use Carbon\Exceptions\InvalidFormatException; use Illuminate\Contracts\Container\BindingResolutionException; @@ -17,6 +19,9 @@ use Illuminate\Http\Request; class ArticleController extends ApiController { + use HasGallery; + + /** * ApplicationController constructor. */ @@ -73,14 +78,19 @@ class ArticleController extends ApiController public function store(ArticleRequest $request) { if (ArticleConductor::creatable() === true) { - $article = Article::create($request->all()); + $article = Article::create($request->except('gallery')); + + if ($request->has('gallery') === true) { + $article->galleryAddMany($request->get('gallery')); + } + return $this->respondAsResource( ArticleConductor::model($request, $article), ['respondCode' => HttpResponseCodes::HTTP_CREATED] ); } else { return $this->respondForbidden(); - } + }//end if } /** @@ -93,7 +103,12 @@ class ArticleController extends ApiController public function update(ArticleRequest $request, Article $article) { if (ArticleConductor::updatable($article) === true) { - $article->update($request->all()); + if ($request->has('gallery') === true) { + $article->gallery()->delete(); + $article->galleryAddMany($request->get('gallery')); + } + + $article->update($request->except('gallery')); return $this->respondAsResource(ArticleConductor::model($request, $article)); } @@ -122,11 +137,8 @@ class ArticleController extends ApiController * @param Request $request The user request. * @param Article $article The article model. * @return JsonResponse Returns the article attachments. - * @throws InvalidFormatException - * @throws BindingResolutionException - * @throws InvalidCastException */ - public function getAttachments(Request $request, Article $article): JsonResponse + public function attachmentIndex(Request $request, Article $article): JsonResponse { if (ArticleConductor::viewable($article) === true) { $medium = $article->attachments->map(function ($attachment) { @@ -145,13 +157,11 @@ class ArticleController extends ApiController * @param Request $request The user request. * @param Article $article The article model. * @return JsonResponse The response. - * @throws BindingResolutionException - * @throws MassAssignmentException */ - public function storeAttachment(Request $request, Article $article): JsonResponse + public function attachmentStore(Request $request, Article $article): JsonResponse { if (ArticleConductor::updatable($article) === true) { - if ($request->has("medium") && Media::find($request->medium)) { + if ($request->has("medium") === true && Media::find($request->medium) !== null) { $article->attachments()->create(['media_id' => $request->medium]); return $this->respondCreated(); } @@ -167,10 +177,9 @@ class ArticleController extends ApiController * * @param Request $request The user request. * @param Article $article The related model. - * @throws BindingResolutionException - * @throws MassAssignmentException + * @return JsonResponse */ - public function updateAttachments(Request $request, Article $article): JsonResponse + public function attachmentUpdate(Request $request, Article $article): JsonResponse { if (ArticleConductor::updatable($article) === true) { $mediaIds = $request->attachments; @@ -183,7 +192,7 @@ class ArticleController extends ApiController // Delete attachments that are not in $mediaIds foreach ($attachments as $attachment) { - if (!in_array($attachment->media_id, $mediaIds)) { + if (in_array($attachment->media_id, $mediaIds) === false) { $attachment->delete(); } } @@ -193,13 +202,13 @@ class ArticleController extends ApiController $found = false; foreach ($attachments as $attachment) { - if ($attachment->media_id == $mediaId) { + if ($attachment->media_id === $mediaId) { $found = true; break; } } - if (!$found) { + if ($found === false) { $article->attachments()->create(['media_id' => $mediaId]); } } @@ -215,9 +224,9 @@ class ArticleController extends ApiController * @param Request $request The user request. * @param Article $article The model. * @param Media $medium The attachment medium. - * @throws BindingResolutionException + * @return JsonResponse */ - public function deleteAttachment(Request $request, Article $article, Media $medium): JsonResponse + public function attachmentDelete(Request $request, Article $article, Media $medium): JsonResponse { if (ArticleConductor::updatable($article) === true) { $attachments = $article->attachments; @@ -231,7 +240,7 @@ class ArticleController extends ApiController } } - if ($deleted) { + if ($deleted === true) { // Attachment was deleted successfully return $this->respondNoContent(); } else { diff --git a/app/Http/Controllers/Api/EventController.php b/app/Http/Controllers/Api/EventController.php index 5a3845d..1684b82 100644 --- a/app/Http/Controllers/Api/EventController.php +++ b/app/Http/Controllers/Api/EventController.php @@ -117,7 +117,7 @@ class EventController extends ApiController * @param Event $event The event model. * @return JsonResponse Returns the event attachments. */ - public function getAttachments(Request $request, Event $event): JsonResponse + public function attachmentIndex(Request $request, Event $event): JsonResponse { if (EventConductor::viewable($event) === true) { $medium = $event->attachments->map(function ($attachment) { @@ -137,7 +137,7 @@ class EventController extends ApiController * @param Event $event The event model. * @return JsonResponse The response. */ - public function storeAttachment(Request $request, Event $event): JsonResponse + public function attachmentStore(Request $request, Event $event): JsonResponse { if (EventConductor::updatable($event) === true) { if ($request->has("medium") === true && Media::find($request->medium) !== null) { @@ -157,7 +157,7 @@ class EventController extends ApiController * @param Request $request The user request. * @param Event $event The related model. */ - public function updateAttachments(Request $request, Event $event): JsonResponse + public function attachmentUpdate(Request $request, Event $event): JsonResponse { if (EventConductor::updatable($event) === true) { $mediaIds = $request->attachments; @@ -204,7 +204,7 @@ class EventController extends ApiController * @param Event $event The model. * @param Media $medium The attachment medium. */ - public function deleteAttachment(Request $request, Event $event, Media $medium): JsonResponse + public function attachmentDelete(Request $request, Event $event, Media $medium): JsonResponse { if (EventConductor::updatable($event) === true) { $attachments = $event->attachments; diff --git a/app/Models/Article.php b/app/Models/Article.php index 5d4f715..3dc2e87 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -2,15 +2,18 @@ namespace App\Models; +use App\Traits\HasGallery; use App\Traits\Uuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphMany; class Article extends Model { use HasFactory; use Uuids; + use HasGallery; /** * The attributes that are mass assignable. @@ -29,6 +32,8 @@ class Article extends Model /** * Get the article user + * + * @return Illuminate\Database\Eloquent\Relations\BelongsTo */ public function user(): BelongsTo { @@ -37,6 +42,8 @@ class Article extends Model /** * Get all of the article's attachments. + * + * @return Illuminate\Database\Eloquent\Relations\MorphMany */ public function attachments(): MorphMany { diff --git a/app/Models/Gallery.php b/app/Models/Gallery.php new file mode 100644 index 0000000..fb0aae6 --- /dev/null +++ b/app/Models/Gallery.php @@ -0,0 +1,45 @@ + + */ + protected $fillable = [ + 'media_id', + ]; + + + /** + * Get gallery addendum model. + * + * @return Illuminate\Database\Eloquent\Relations\MorphTo Addenum model. + */ + public function addendum(): MorphTo + { + return $this->morphTo(); + } + + /** + * Get the media for this item. + * + * @return Illuminate\Database\Eloquent\Relations\BelongsTo The media model. + */ + public function media(): BelongsTo + { + return $this->belongsTo(Media::class); + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 29fc9dd..3499f12 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -61,28 +61,27 @@ class RouteServiceProvider extends ServiceProvider Route::macro('apiAddendumResource', function ($addendum, $uri, $controller) { $singularUri = Str::singular($uri); - $pluralAddendumLC = strtolower(Str::plural($addendum)); - $pluralAddendumTC = ucfirst($pluralAddendumLC); - $singularAddendumTC = Str::singular($pluralAddendumTC); + $signularAddendum = Str::singular((strtolower($addendum))); + $pluralAddendum = Str::plural($signularAddendum); - Route::get("$uri/{{$singularUri}}/{{$pluralAddendumLC}}", [$controller, "get{{$pluralAddendumTC}}"]) - ->name("{{$singularUri}}.{{$pluralAddendumLC}}.index"); + Route::get("{$uri}/{{$singularUri}}/{$pluralAddendum}", [$controller, "{$signularAddendum}Index"]) + ->name("{$singularUri}.{$signularAddendum}.index"); - Route::post("$uri/{{$singularUri}}/{{$pluralAddendumLC}}", [$controller, "store{{$singularAddendumTC}}"]) - ->name("{{$singularUri}}.{{$pluralAddendumLC}}.store"); + Route::post("{$uri}/{{$singularUri}}/{$pluralAddendum}", [$controller, "{$signularAddendum}Store"]) + ->name("{$singularUri}.{$signularAddendum}.store"); Route::match( ['put', 'patch'], - "$uri/{{$singularUri}}/{{$pluralAddendumLC}}", - [$controller, "update{{$pluralAddendumTC}}"] + "{$uri}/{{$singularUri}}/{$pluralAddendum}", + [$controller, "{$signularAddendum}Update"] ) - ->name("{{$singularUri}}.{{$pluralAddendumLC}}.update"); + ->name("{$singularUri}.{$signularAddendum}.update"); Route::delete( - "$uri/{{$singularUri}}/{{$pluralAddendumLC}}/{medium}", - [$controller,"delete{{$singularAddendumTC}}"] + "{$uri}/{{$singularUri}}/{$pluralAddendum}/{medium}", + [$controller,"{$signularAddendum}Delete"] ) - ->name("{{$singularUri}}.{{$pluralAddendumLC}}.destroy"); + ->name("{$singularUri}.{$signularAddendum}.destroy"); }); } } diff --git a/app/Traits/HasGallery.php b/app/Traits/HasGallery.php new file mode 100644 index 0000000..62c9129 --- /dev/null +++ b/app/Traits/HasGallery.php @@ -0,0 +1,63 @@ +gallery()->delete(); + }); + } + + /** + * Add multiple gallery items to the model. + * + * @param array|string $ids The media ids to add. + * @param string $delimiter The split delimiter if $ids is a string. + * @param boolean $allowDuplicates Whether to allow duplicate media IDs or not. + * @return void + */ + public function galleryAddMany(array|string $ids, string $delimiter = ',', bool $allowDuplicates = false): void + { + if (is_array($ids) === false) { + $ids = explode($delimiter, $ids); + } + + $ids = array_map('trim', $ids); + $existingIds = $this->gallery()->pluck('media_id')->toArray(); + + $galleryItems = []; + foreach ($ids as $id) { + if ($allowDuplicates === false && in_array($id, $existingIds) === true) { + continue; + } + + $media = Media::find($id); + if ($media !== null) { + $galleryItems[] = ['media_id' => $id]; + } + } + + $this->gallery()->createMany($galleryItems); + } + + /** + * Get the article's gallery. + * + * @return Illuminate\Database\Eloquent\Relations\MorphMany The gallery items + */ + public function gallery(): MorphMany + { + return $this->morphMany(\App\Models\Gallery::class, 'addendum'); + } +} diff --git a/database/migrations/2023_07_16_095623_create_galleries_table.php b/database/migrations/2023_07_16_095623_create_galleries_table.php new file mode 100644 index 0000000..c503ac3 --- /dev/null +++ b/database/migrations/2023_07_16_095623_create_galleries_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + $table->uuid('media_id'); + $table->uuidMorphs('addendum'); + $table->timestamps(); + + $table->foreign('media_id')->references('id')->on('media')->onDelete('cascade'); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('galleries'); + } +}; diff --git a/resources/js/components/SMImageGallery.vue b/resources/js/components/SMImageGallery.vue index e4faa56..fc78eac 100644 --- a/resources/js/components/SMImageGallery.vue +++ b/resources/js/components/SMImageGallery.vue @@ -1,17 +1,53 @@ diff --git a/resources/js/components/dialogs/SMDialogMedia.vue b/resources/js/components/dialogs/SMDialogMedia.vue index 0f3145f..7e4cd36 100644 --- a/resources/js/components/dialogs/SMDialogMedia.vue +++ b/resources/js/components/dialogs/SMDialogMedia.vue @@ -357,18 +357,6 @@ " small class="bg-white bg-op-90 w-full h-full" /> - diff --git a/resources/js/helpers/api.types.ts b/resources/js/helpers/api.types.ts index 0a9992e..4c4f269 100644 --- a/resources/js/helpers/api.types.ts +++ b/resources/js/helpers/api.types.ts @@ -96,6 +96,7 @@ export interface Article { content: string; publish_at: string; hero: Media; + gallery: Array; attachments: Array; } diff --git a/resources/js/views/Article.vue b/resources/js/views/Article.vue index f701d37..4fa019e 100644 --- a/resources/js/views/Article.vue +++ b/resources/js/views/Article.vue @@ -36,7 +36,12 @@ > - + + @@ -54,6 +59,7 @@ import { userHasPermission } from "../helpers/utils"; import SMLoading from "../components/SMLoading.vue"; import SMPageStatus from "../components/SMPageStatus.vue"; import SMHTML from "../components/SMHTML.vue"; +import SMImageGallery from "../components/SMImageGallery.vue"; const applicationStore = useApplicationStore(); @@ -61,8 +67,18 @@ const applicationStore = useApplicationStore(); * The article data. */ let article: Ref
= ref({ + id: "", + created_at: "", + updated_at: "", title: "", + slug: "", + user_id: "", user: { display_name: "" }, + content: "", + publish_at: "", + hero: {}, + gallery: [], + attachments: [], }); /** diff --git a/resources/js/views/Blog.vue b/resources/js/views/Blog.vue index becb96a..128cbf0 100644 --- a/resources/js/views/Blog.vue +++ b/resources/js/views/Blog.vue @@ -137,7 +137,7 @@ watch( () => articlesPage.value, () => { handleLoad(); - } + }, ); handleLoad(); diff --git a/resources/js/views/dashboard/ArticleEdit.vue b/resources/js/views/dashboard/ArticleEdit.vue index b12f0f3..81586f5 100644 --- a/resources/js/views/dashboard/ArticleEdit.vue +++ b/resources/js/views/dashboard/ArticleEdit.vue @@ -38,6 +38,15 @@ class="mb-8" v-model:model-value="form.controls.content.value" /> +
+

Gallery

+

+ {{ gallery.length }} image{{ + gallery.length != 1 ? "s" : "" + }} +

+ +
{ @@ -164,8 +175,10 @@ const loadData = async () => { attachments.value = (data.article.attachments || []).map( function (attachment) { return attachment.id.toString(); - } + }, ); + + gallery.value = data.article.gallery; } else { pageError.value = 404; } @@ -183,11 +196,12 @@ const handleSubmit = async () => { title: form.controls.title.value, slug: form.controls.slug.value, publish_at: new SMDate( - form.controls.publish_at.value as string + form.controls.publish_at.value as string, ).format("yyyy/MM/dd HH:mm:ss", { utc: true }), user_id: form.controls.user_id.value, content: form.controls.content.value, hero: form.controls.hero.value.id, + gallery: gallery.value.map((item) => item.id), }; let article_id = ""; @@ -280,7 +294,7 @@ const attachmentAdd = async (event) => { }, progress: (progressEvent) => event.attachment.setUploadProgress( - (progressEvent.loaded * progressEvent.total) / 100 + (progressEvent.loaded * progressEvent.total) / 100, ), }); @@ -292,7 +306,7 @@ const attachmentAdd = async (event) => { event.preventDefault(); alert( err.response?.data?.message || - "An unexpected server error occurred" + "An unexpected server error occurred", ); } } diff --git a/routes/api.php b/routes/api.php index fff3675..81fd40c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -44,10 +44,10 @@ Route::apiResource('media', MediaController::class); Route::get('media/{medium}/download', [MediaController::class, 'download']); Route::apiResource('articles', ArticleController::class); -Route::apiAddendumResource('attachment', 'articles', ArticleController::class); +Route::apiAddendumResource('attachments', 'articles', ArticleController::class); Route::apiResource('events', EventController::class); -Route::apiAddendumResource('attachment', 'events', EventController::class); +Route::apiAddendumResource('attachments', 'events', EventController::class); Route::get('/events/{event}/users', [EventController::class, 'userList']); Route::post('/events/{event}/users', [EventController::class, 'userAdd']);