diff --git a/config/services.yaml b/config/services.yaml index 953c650..5f68083 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -82,6 +82,10 @@ services: arguments: $model: '%env(AI_TEXT_MODEL)%' + App\Infrastructure\Http\Controller\Admin\PhotoServeController: + arguments: + $uploadsDir: '%kernel.project_dir%/var/uploads' + App\Infrastructure\Search\WebSearchInterface: alias: App\Infrastructure\Search\TavilyWebSearch diff --git a/src/Application/Article/PhotoService.php b/src/Application/Article/PhotoService.php index 6a1cdd5..ec49aa7 100644 --- a/src/Application/Article/PhotoService.php +++ b/src/Application/Article/PhotoService.php @@ -55,6 +55,25 @@ final class PhotoService } } + /** + * @param list $orderedPhotoIds UUIDs in the desired display order + */ + public function reorder(Uuid $articleId, array $orderedPhotoIds): void + { + $photos = $this->photoRepository->findByArticle($articleId); + $indexed = []; + foreach ($photos as $photo) { + $indexed[$photo->getId()->toRfc4122()] = $photo; + } + + foreach ($orderedPhotoIds as $position => $id) { + if (isset($indexed[$id])) { + $indexed[$id]->setSortOrder($position); + $this->photoRepository->save($indexed[$id]); + } + } + } + public function delete(Uuid $photoId): void { $photo = $this->photoRepository->findById($photoId) diff --git a/src/Infrastructure/Http/Controller/Admin/ArticleCrudController.php b/src/Infrastructure/Http/Controller/Admin/ArticleCrudController.php index d7855d7..e69840e 100644 --- a/src/Infrastructure/Http/Controller/Admin/ArticleCrudController.php +++ b/src/Infrastructure/Http/Controller/Admin/ArticleCrudController.php @@ -139,6 +139,10 @@ final class ArticleCrudController extends AbstractCrudController ->setEntryIsComplex(false) ->allowAdd(false) ->allowDelete(false); + + yield TextField::new('photos', new TranslatableMessage('field.photos', [], 'admin')) + ->setTemplatePath('admin/field/photos.html.twig') + ->onlyOnDetail(); } #[AdminRoute('/rerun-ai', name: 'rerunAi')] diff --git a/src/Infrastructure/Http/Controller/Admin/PhotoServeController.php b/src/Infrastructure/Http/Controller/Admin/PhotoServeController.php new file mode 100644 index 0000000..162e1a9 --- /dev/null +++ b/src/Infrastructure/Http/Controller/Admin/PhotoServeController.php @@ -0,0 +1,31 @@ + '[a-zA-Z0-9\-\.]+'], methods: ['GET'])] + public function serve(string $filename): Response + { + $path = rtrim($this->uploadsDir, '/').'/'.$filename; + + if (!file_exists($path) || !is_file($path)) { + throw $this->createNotFoundException('Photo not found.'); + } + + return new BinaryFileResponse($path); + } +} diff --git a/src/Infrastructure/Http/Controller/Api/PhotoController.php b/src/Infrastructure/Http/Controller/Api/PhotoController.php index 8f35877..8970ed5 100644 --- a/src/Infrastructure/Http/Controller/Api/PhotoController.php +++ b/src/Infrastructure/Http/Controller/Api/PhotoController.php @@ -62,6 +62,26 @@ final class PhotoController extends AbstractController return $this->json(null, Response::HTTP_NO_CONTENT); } + #[Route('/sort', name: 'sort', methods: ['POST'])] + public function sort(string $articleId, Request $request): JsonResponse + { + /** @var array{order?: list} $body */ + $body = $request->toArray(); + $order = $body['order'] ?? []; + + if ([] === $order) { + return $this->json(['error' => 'order array required'], Response::HTTP_BAD_REQUEST); + } + + try { + $this->service->reorder(Uuid::fromString($articleId), $order); + } catch (\DomainException $e) { + return $this->json(['error' => $e->getMessage()], Response::HTTP_NOT_FOUND); + } + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + #[Route('/{photoId}', name: 'delete', methods: ['DELETE'])] public function delete(string $articleId, string $photoId): JsonResponse { diff --git a/templates/admin/field/photos.html.twig b/templates/admin/field/photos.html.twig new file mode 100644 index 0000000..09fa79a --- /dev/null +++ b/templates/admin/field/photos.html.twig @@ -0,0 +1,213 @@ +{# Article photo gallery — shown on detail page #} +{% set article = ea.entity.instance %} +{% set articleId = article.id.toRfc4122() %} +{% set photos = article.photos|sort((a, b) => a.sortOrder <=> b.sortOrder) %} + + + + + + + diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index 42584d0..17ac517 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -71,6 +71,7 @@ field.prompt_key_help: 'Eindeutiger Bezeichner für den Prompt (z. B. spec field.prompt_body: 'Prompt-Text' field.prompt_body_preview: Vorschau field.last_updated: 'Zuletzt geändert' +field.photos: Fotos field.locale: Sprache field.domain: Bereich field.translation_key: Schlüssel diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index d4531ea..f2d71a2 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -71,6 +71,7 @@ field.prompt_key_help: 'Slug identifying the prompt (e.g. specs_research