diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 1cfc205..1d6853f 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -57,8 +57,7 @@ class ElementController public function update(Request $request): JsonResponse { - $iconImage = $this->uploadedFileInput($request, 'iconImage'); - $pdf = $this->uploadedFileInput($request, 'pdf'); + $file = $this->uploadedFileInput($request, 'file'); try { $element = $this->updateElement->execute( @@ -71,16 +70,15 @@ class ElementController 'iconImageUrl', ), richText: $this->stringInput($request, 'richText'), + shortPdfPath: $this->stringInput($request, 'shortPdfPath'), + longPdfPath: $this->stringInput($request, 'longPdfPath'), pdfPath: $this->stringInput($request, 'pdfPath'), youtubeUrl: $this->stringInput($request, 'youtubeUrl'), - iconImageContents: $iconImage['contents'], - iconImageOriginalName: $iconImage['originalName'], - iconImageMimeType: $iconImage['mimeType'], - iconImageSizeBytes: $iconImage['sizeBytes'], - pdfContents: $pdf['contents'], - pdfOriginalName: $pdf['originalName'], - pdfMimeType: $pdf['mimeType'], - pdfSizeBytes: $pdf['sizeBytes'], + fileType: $this->stringInput($request, 'fileType'), + fileContents: $file['contents'], + fileOriginalName: $file['originalName'], + fileMimeType: $file['mimeType'], + fileSizeBytes: $file['sizeBytes'], ) ); } catch (BadRequestException $exception) { @@ -176,6 +174,8 @@ class ElementController * description: string, * iconImageUrl: string|null, * richText: string, + * shortPdfPath: string|null, + * longPdfPath: string|null, * pdfPath: string|null, * youtubeUrl: string|null, * } @@ -191,8 +191,16 @@ class ElementController 'element-icons/', ), 'richText' => $element->getRichText(), + 'shortPdfPath' => $this->storageUrl( + $element->getShortPdfPath(), + 'element-pdfs/', + ), + 'longPdfPath' => $this->storageUrl( + $element->getLongPdfPath(), + 'element-pdfs/', + ), 'pdfPath' => $this->storageUrl( - $element->getPdfPath(), + $element->getShortPdfPath(), 'element-pdfs/', ), 'youtubeUrl' => $element->getYoutubeUrl(), diff --git a/backend/app/Element/CreateElementDto.php b/backend/app/Element/CreateElementDto.php index b5a9b7f..3d41e23 100644 --- a/backend/app/Element/CreateElementDto.php +++ b/backend/app/Element/CreateElementDto.php @@ -12,7 +12,8 @@ class CreateElementDto public string $description, public ?string $iconImageUrl, public string $richText, - public ?string $pdfPath, + public ?string $shortPdfPath, + public ?string $longPdfPath, public ?string $youtubeUrl, public ?Element $parentElement, ) { diff --git a/backend/app/Element/Element.php b/backend/app/Element/Element.php index 37be364..7b07975 100644 --- a/backend/app/Element/Element.php +++ b/backend/app/Element/Element.php @@ -12,7 +12,8 @@ class Element private string $description, private ?string $iconImageUrl, private string $richText, - private ?string $pdfPath, + private ?string $shortPdfPath, + private ?string $longPdfPath, private ?string $youtubeUrl, private Set $set, private ?Element $parentElement, @@ -64,14 +65,24 @@ class Element $this->richText = $richText; } - public function getPdfPath(): ?string + public function getShortPdfPath(): ?string { - return $this->pdfPath; + return $this->shortPdfPath; } - public function setPdfPath(?string $pdfPath): void + public function setShortPdfPath(?string $shortPdfPath): void { - $this->pdfPath = $pdfPath; + $this->shortPdfPath = $shortPdfPath; + } + + public function getLongPdfPath(): ?string + { + return $this->longPdfPath; + } + + public function setLongPdfPath(?string $longPdfPath): void + { + $this->longPdfPath = $longPdfPath; } public function getYoutubeUrl(): ?string diff --git a/backend/app/Element/ElementModel.php b/backend/app/Element/ElementModel.php index a71eb37..3a8a435 100644 --- a/backend/app/Element/ElementModel.php +++ b/backend/app/Element/ElementModel.php @@ -12,7 +12,8 @@ use Illuminate\Database\Eloquent\Model; * @property string $description * @property string|null $icon_image_url * @property string $rich_text - * @property string|null $pdf_path + * @property string|null $short_pdf_path + * @property string|null $long_pdf_path * @property string|null $youtube_url * @property int|null $parent_element_id * @@ -26,7 +27,8 @@ use Illuminate\Database\Eloquent\Model; * @method static Builder|ElementModel whereDescription($value) * @method static Builder|ElementModel whereIconImageUrl($value) * @method static Builder|ElementModel whereRichText($value) - * @method static Builder|ElementModel wherePdfPath($value) + * @method static Builder|ElementModel whereShortPdfPath($value) + * @method static Builder|ElementModel whereLongPdfPath($value) * @method static Builder|ElementModel whereYoutubeUrl($value) * * @mixin \Eloquent @@ -43,7 +45,8 @@ class ElementModel extends Model 'description', 'icon_image_url', 'rich_text', - 'pdf_path', + 'short_pdf_path', + 'long_pdf_path', 'youtube_url', 'parent_element_id', ]; diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index 379abe0..771bc4c 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -20,7 +20,8 @@ class EloquentElementRepository implements ElementRepository 'description' => $dto->description, 'icon_image_url' => $dto->iconImageUrl, 'rich_text' => $dto->richText, - 'pdf_path' => $dto->pdfPath, + 'short_pdf_path' => $dto->shortPdfPath, + 'long_pdf_path' => $dto->longPdfPath, 'youtube_url' => $dto->youtubeUrl, 'parent_element_id' => $dto->parentElement?->getId(), ]); @@ -31,7 +32,8 @@ class EloquentElementRepository implements ElementRepository description: $dto->description, iconImageUrl: $dto->iconImageUrl, richText: $dto->richText, - pdfPath: $dto->pdfPath, + shortPdfPath: $dto->shortPdfPath, + longPdfPath: $dto->longPdfPath, youtubeUrl: $dto->youtubeUrl, set: $dto->set, parentElement: $dto->parentElement, @@ -52,7 +54,8 @@ class EloquentElementRepository implements ElementRepository $model->description = $element->getDescription(); $model->icon_image_url = $element->getIconImageUrl(); $model->rich_text = $element->getRichText(); - $model->pdf_path = $element->getPdfPath(); + $model->short_pdf_path = $element->getShortPdfPath(); + $model->long_pdf_path = $element->getLongPdfPath(); $model->youtube_url = $element->getYoutubeUrl(); $model->parent_element_id = $element->getParentElement()?->getId(); $model->save(); @@ -137,7 +140,8 @@ class EloquentElementRepository implements ElementRepository description: $model->description, iconImageUrl: $model->icon_image_url, richText: $model->rich_text, - pdfPath: $model->pdf_path, + shortPdfPath: $model->short_pdf_path, + longPdfPath: $model->long_pdf_path, youtubeUrl: $model->youtube_url, set: $set, parentElement: $parentElement, diff --git a/backend/app/Element/UseCases/CreateElement/CreateElement.php b/backend/app/Element/UseCases/CreateElement/CreateElement.php index e91a0ac..3929bce 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElement.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElement.php @@ -35,7 +35,12 @@ class CreateElement ? null : $request->iconImageUrl; $richText = $request->richText ?? ''; - $pdfPath = $request->pdfPath === '' ? null : $request->pdfPath; + $shortPdfPath = $request->shortPdfPath === '' + ? null + : $request->shortPdfPath; + $longPdfPath = $request->longPdfPath === '' + ? null + : $request->longPdfPath; $youtubeUrl = $request->youtubeUrl === '' ? null : $request->youtubeUrl; @@ -56,7 +61,8 @@ class CreateElement description: $description, iconImageUrl: $iconImageUrl, richText: $richText, - pdfPath: $pdfPath, + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, youtubeUrl: $youtubeUrl, parentElement: null, )); @@ -82,7 +88,8 @@ class CreateElement description: $description, iconImageUrl: $iconImageUrl, richText: $richText, - pdfPath: $pdfPath, + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); diff --git a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php index ef689cc..3f6d53f 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php @@ -10,7 +10,8 @@ class CreateElementRequest public ?string $description, public ?string $iconImageUrl, public ?string $richText, - public ?string $pdfPath, + public ?string $shortPdfPath, + public ?string $longPdfPath, public ?string $youtubeUrl, public ?int $parentElementId, ) { diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElement.php b/backend/app/Element/UseCases/UpdateElement/UpdateElement.php index 0533b16..aab9cb8 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateElement.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateElement.php @@ -9,15 +9,23 @@ use App\Exceptions\NotFoundException; class UpdateElement { + private const FILE_TYPE_ICON_IMAGE = 'iconImage'; + + private const FILE_TYPE_SHORT_PDF = 'shortPdf'; + + private const FILE_TYPE_LONG_PDF = 'longPdf'; + public function __construct( private UpdateTitle $updateTitle, private UpdateDescription $updateDescription, private UpdateIconImageUrl $updateIconImageUrl, private UpdateRichText $updateRichText, - private UpdatePdfPath $updatePdfPath, + private UpdateShortPdfPath $updateShortPdfPath, + private UpdateLongPdfPath $updateLongPdfPath, private UpdateYoutubeUrl $updateYoutubeUrl, private UpdateIconImage $updateIconImage, - private UpdatePdf $updatePdf, + private UpdateShortPdf $updateShortPdf, + private UpdateLongPdf $updateLongPdf, private ElementRepository $elementRepository, ) { } @@ -36,10 +44,15 @@ class UpdateElement && $request->description === null && $request->iconImageUrl === null && $request->richText === null + && $request->shortPdfPath === null + && $request->longPdfPath === null && $request->pdfPath === null && $request->youtubeUrl === null - && $request->iconImageContents === null - && $request->pdfContents === null; + && $request->fileType === null + && $request->fileContents === null + && $request->fileOriginalName === null + && $request->fileMimeType === null + && $request->fileSizeBytes === null; if ($hasNoFields) { throw new BadRequestException('nothing to update'); } @@ -70,10 +83,21 @@ class UpdateElement richText: $request->richText, )); } - if ($request->pdfPath !== null) { - $this->updatePdfPath->execute(new UpdatePdfPathRequest( + if ($request->shortPdfPath !== null) { + $this->updateShortPdfPath->execute(new UpdateShortPdfPathRequest( id: $request->id, - pdfPath: $request->pdfPath, + shortPdfPath: $request->shortPdfPath, + )); + } elseif ($request->pdfPath !== null) { + $this->updateShortPdfPath->execute(new UpdateShortPdfPathRequest( + id: $request->id, + shortPdfPath: $request->pdfPath, + )); + } + if ($request->longPdfPath !== null) { + $this->updateLongPdfPath->execute(new UpdateLongPdfPathRequest( + id: $request->id, + longPdfPath: $request->longPdfPath, )); } if ($request->youtubeUrl !== null) { @@ -82,27 +106,8 @@ class UpdateElement youtubeUrl: $request->youtubeUrl, )); } - if ($request->iconImageContents !== null) { - $this->updateIconImage->execute( - new UpdateIconImageRequest( - id: $request->id, - iconImageContents: $request->iconImageContents, - iconImageOriginalName: $request->iconImageOriginalName, - iconImageMimeType: $request->iconImageMimeType, - iconImageSizeBytes: $request->iconImageSizeBytes, - ) - ); - } - if ($request->pdfContents !== null) { - $this->updatePdf->execute( - new UpdatePdfRequest( - id: $request->id, - pdfContents: $request->pdfContents, - pdfOriginalName: $request->pdfOriginalName, - pdfMimeType: $request->pdfMimeType, - pdfSizeBytes: $request->pdfSizeBytes, - ) - ); + if ($this->hasFileInput($request)) { + $this->updateFile($request); } $element = $this->elementRepository->find($request->id); @@ -112,4 +117,68 @@ class UpdateElement return $element; } + + /** + * @throws BadRequestException + * @throws NotFoundException + */ + private function updateFile(UpdateElementRequest $request): void + { + if ($request->fileType === null) { + throw new BadRequestException('fileType is required'); + } + + if ($request->fileType === self::FILE_TYPE_ICON_IMAGE) { + $this->updateIconImage->execute( + new UpdateIconImageRequest( + id: $request->id, + fileContents: $request->fileContents, + fileOriginalName: $request->fileOriginalName, + fileMimeType: $request->fileMimeType, + fileSizeBytes: $request->fileSizeBytes, + ) + ); + + return; + } + + if ($request->fileType === self::FILE_TYPE_SHORT_PDF) { + $this->updateShortPdf->execute( + new UpdateShortPdfRequest( + id: $request->id, + fileContents: $request->fileContents, + fileOriginalName: $request->fileOriginalName, + fileMimeType: $request->fileMimeType, + fileSizeBytes: $request->fileSizeBytes, + ) + ); + + return; + } + + if ($request->fileType === self::FILE_TYPE_LONG_PDF) { + $this->updateLongPdf->execute( + new UpdateLongPdfRequest( + id: $request->id, + fileContents: $request->fileContents, + fileOriginalName: $request->fileOriginalName, + fileMimeType: $request->fileMimeType, + fileSizeBytes: $request->fileSizeBytes, + ) + ); + + return; + } + + throw new BadRequestException('fileType is invalid'); + } + + private function hasFileInput(UpdateElementRequest $request): bool + { + return $request->fileType !== null + || $request->fileContents !== null + || $request->fileOriginalName !== null + || $request->fileMimeType !== null + || $request->fileSizeBytes !== null; + } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php index aa1aa06..efa1519 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php @@ -10,16 +10,15 @@ class UpdateElementRequest public ?string $description, public ?string $iconImageUrl, public ?string $richText, + public ?string $shortPdfPath, + public ?string $longPdfPath, public ?string $pdfPath, public ?string $youtubeUrl, - public ?string $iconImageContents, - public ?string $iconImageOriginalName, - public ?string $iconImageMimeType, - public ?int $iconImageSizeBytes, - public ?string $pdfContents, - public ?string $pdfOriginalName, - public ?string $pdfMimeType, - public ?int $pdfSizeBytes, + public ?string $fileType, + public ?string $fileContents, + public ?string $fileOriginalName, + public ?string $fileMimeType, + public ?int $fileSizeBytes, ) { } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php b/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php index 45d274f..59dcaad 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php @@ -36,17 +36,17 @@ class UpdateIconImage } if ( - $request->iconImageContents === null - || $request->iconImageOriginalName === null - || $request->iconImageMimeType === null - || $request->iconImageSizeBytes === null + $request->fileContents === null + || $request->fileOriginalName === null + || $request->fileMimeType === null + || $request->fileSizeBytes === null ) { throw new BadRequestException('icon image is required'); } if ( ! in_array( - $request->iconImageMimeType, + $request->fileMimeType, self::ALLOWED_MIME_TYPES, true, ) @@ -56,7 +56,7 @@ class UpdateIconImage ); } - if ($request->iconImageSizeBytes > self::MAX_SIZE_BYTES) { + if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) { throw new BadRequestException( 'icon image must be 5MB or smaller', ); @@ -68,10 +68,10 @@ class UpdateIconImage } $iconImage = new FileToUpload( - contents: $request->iconImageContents, - originalName: $request->iconImageOriginalName, - mimeType: $request->iconImageMimeType, - sizeBytes: $request->iconImageSizeBytes, + contents: $request->fileContents, + originalName: $request->fileOriginalName, + mimeType: $request->fileMimeType, + sizeBytes: $request->fileSizeBytes, ); $path = $this->fileUploader->upload($iconImage, 'element-icons'); $element->setIconImageUrl($path); diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php index a119053..ccf973d 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php @@ -6,10 +6,10 @@ class UpdateIconImageRequest { public function __construct( public ?int $id, - public ?string $iconImageContents, - public ?string $iconImageOriginalName, - public ?string $iconImageMimeType, - public ?int $iconImageSizeBytes, + public ?string $fileContents, + public ?string $fileOriginalName, + public ?string $fileMimeType, + public ?int $fileSizeBytes, ) { } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdf.php b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdf.php similarity index 67% rename from backend/app/Element/UseCases/UpdateElement/UpdatePdf.php rename to backend/app/Element/UseCases/UpdateElement/UpdateLongPdf.php index 0a13f09..014ada9 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdf.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdf.php @@ -9,7 +9,7 @@ use App\Exceptions\NotFoundException; use App\Shared\Files\FileToUpload; use App\Shared\Files\FileUploader; -class UpdatePdf +class UpdateLongPdf { private const ALLOWED_MIME_TYPES = [ 'application/pdf', @@ -27,24 +27,24 @@ class UpdatePdf * @throws BadRequestException * @throws NotFoundException */ - public function execute(UpdatePdfRequest $request): Element + public function execute(UpdateLongPdfRequest $request): Element { if ($request->id === null) { throw new BadRequestException('id is required'); } if ( - $request->pdfContents === null - || $request->pdfOriginalName === null - || $request->pdfMimeType === null - || $request->pdfSizeBytes === null + $request->fileContents === null + || $request->fileOriginalName === null + || $request->fileMimeType === null + || $request->fileSizeBytes === null ) { throw new BadRequestException('pdf is required'); } if ( ! in_array( - $request->pdfMimeType, + $request->fileMimeType, self::ALLOWED_MIME_TYPES, true, ) @@ -52,7 +52,7 @@ class UpdatePdf throw new BadRequestException('pdf must be a pdf file'); } - if ($request->pdfSizeBytes > self::MAX_SIZE_BYTES) { + if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) { throw new BadRequestException('pdf must be 10MB or smaller'); } @@ -62,13 +62,13 @@ class UpdatePdf } $pdf = new FileToUpload( - contents: $request->pdfContents, - originalName: $request->pdfOriginalName, - mimeType: $request->pdfMimeType, - sizeBytes: $request->pdfSizeBytes, + contents: $request->fileContents, + originalName: $request->fileOriginalName, + mimeType: $request->fileMimeType, + sizeBytes: $request->fileSizeBytes, ); - $path = $this->fileUploader->upload($pdf, 'element-pdfs'); - $element->setPdfPath($path); + $path = $this->fileUploader->upload($pdf, 'element-pdfs/long'); + $element->setLongPdfPath($path); return $this->elementRepository->update($element); } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPath.php similarity index 73% rename from backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php rename to backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPath.php index 404c78b..ae04422 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPath.php @@ -8,7 +8,7 @@ use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; use App\Shared\Files\FileUploader; -class UpdatePdfPath +class UpdateLongPdfPath { public function __construct( private ElementRepository $elementRepository, @@ -20,14 +20,14 @@ class UpdatePdfPath * @throws BadRequestException * @throws NotFoundException */ - public function execute(UpdatePdfPathRequest $request): Element + public function execute(UpdateLongPdfPathRequest $request): Element { if ($request->id === null) { throw new BadRequestException('id is required'); } - if ($request->pdfPath === null) { - throw new BadRequestException('pdfPath is required'); + if ($request->longPdfPath === null) { + throw new BadRequestException('longPdfPath is required'); } $element = $this->elementRepository->find($request->id); @@ -35,12 +35,12 @@ class UpdatePdfPath throw new NotFoundException('Element not found'); } - $pdfPath = $this->nullableString($request->pdfPath); - if ($pdfPath === null) { - $this->deleteManagedFile($element->getPdfPath()); + $longPdfPath = $this->nullableString($request->longPdfPath); + if ($longPdfPath === null) { + $this->deleteManagedFile($element->getLongPdfPath()); } - $element->setPdfPath($pdfPath); + $element->setLongPdfPath($longPdfPath); return $this->elementRepository->update($element); } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPathRequest.php similarity index 65% rename from backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php rename to backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPathRequest.php index c48c47d..4f4a064 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPathRequest.php @@ -2,11 +2,11 @@ namespace App\Element\UseCases\UpdateElement; -class UpdatePdfPathRequest +class UpdateLongPdfPathRequest { public function __construct( public ?int $id, - public ?string $pdfPath, + public ?string $longPdfPath, ) { } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfRequest.php new file mode 100644 index 0000000..eb891a6 --- /dev/null +++ b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfRequest.php @@ -0,0 +1,15 @@ +id === null) { + throw new BadRequestException('id is required'); + } + + if ( + $request->fileContents === null + || $request->fileOriginalName === null + || $request->fileMimeType === null + || $request->fileSizeBytes === null + ) { + throw new BadRequestException('pdf is required'); + } + + if ( + ! in_array( + $request->fileMimeType, + self::ALLOWED_MIME_TYPES, + true, + ) + ) { + throw new BadRequestException('pdf must be a pdf file'); + } + + if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) { + throw new BadRequestException('pdf must be 10MB or smaller'); + } + + $element = $this->elementRepository->find($request->id); + if ($element === null) { + throw new NotFoundException('Element not found'); + } + + $pdf = new FileToUpload( + contents: $request->fileContents, + originalName: $request->fileOriginalName, + mimeType: $request->fileMimeType, + sizeBytes: $request->fileSizeBytes, + ); + $path = $this->fileUploader->upload($pdf, 'element-pdfs/short'); + $element->setShortPdfPath($path); + + return $this->elementRepository->update($element); + } +} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPath.php b/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPath.php new file mode 100644 index 0000000..ae4b4e3 --- /dev/null +++ b/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPath.php @@ -0,0 +1,65 @@ +id === null) { + throw new BadRequestException('id is required'); + } + + if ($request->shortPdfPath === null) { + throw new BadRequestException('shortPdfPath is required'); + } + + $element = $this->elementRepository->find($request->id); + if ($element === null) { + throw new NotFoundException('Element not found'); + } + + $shortPdfPath = $this->nullableString($request->shortPdfPath); + if ($shortPdfPath === null) { + $this->deleteManagedFile($element->getShortPdfPath()); + } + + $element->setShortPdfPath($shortPdfPath); + + return $this->elementRepository->update($element); + } + + private function nullableString(string $value): ?string + { + if ($value === '') { + return null; + } + + return $value; + } + + private function deleteManagedFile(?string $path): void + { + if ($path === null) { + return; + } + + $this->fileUploader->delete($path); + } +} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPathRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPathRequest.php new file mode 100644 index 0000000..a10555f --- /dev/null +++ b/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPathRequest.php @@ -0,0 +1,12 @@ +text('description')->default(''); $table->string('icon_image_url')->nullable(); $table->text('rich_text')->default(''); - $table->string('pdf_path')->nullable(); + $table->string('short_pdf_path')->nullable(); + $table->string('long_pdf_path')->nullable(); $table->string('youtube_url')->nullable(); $table->foreignId('parent_element_id') ->nullable() diff --git a/backend/database/seeders/ElementSeeder.php b/backend/database/seeders/ElementSeeder.php index c7df012..ba5aded 100644 --- a/backend/database/seeders/ElementSeeder.php +++ b/backend/database/seeders/ElementSeeder.php @@ -20,7 +20,7 @@ class ElementSeeder extends Seeder 'element-icons/baderech-haavodah-icon.png', ); $introductionPdfPath = $this->seedStorageFile( - 'element-pdfs/baderech.pdf', + 'element-pdfs/short/baderech.pdf', ); $rootElement = $elementRepository->create(new CreateElementDto( @@ -29,7 +29,8 @@ class ElementSeeder extends Seeder description: $baderechSet->getDescription(), iconImageUrl: $rootIconImageUrl, richText: '', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElement: null, )); @@ -44,7 +45,7 @@ class ElementSeeder extends Seeder . 'unlock the ability to live with strength, confidence, ' . 'and hope.', 'richText' => $this->introductionRichText(), - 'pdfPath' => $introductionPdfPath, + 'shortPdfPath' => $introductionPdfPath, ], [ 'title' => '2. Foundations', @@ -54,7 +55,7 @@ class ElementSeeder extends Seeder . 'ability to uplift the physical world with spiritual ' . 'intention.', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], [ 'title' => '3. Divine Plan', @@ -62,14 +63,14 @@ class ElementSeeder extends Seeder . 'generation and the call to remain hopeful, ' . 'optimistic, and resilient.', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], [ 'title' => '4. Architecture of the Soul', 'description' => "Becoming a Baal Da'at - A Master of " . 'Inner Awareness', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], [ 'title' => '5. Arba Yesodot', @@ -77,7 +78,7 @@ class ElementSeeder extends Seeder . 'Physical & Financial Health, Relationships, Wisdom ' . 'and Guidance.', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], [ 'title' => '6. Fluid Integration', @@ -85,7 +86,7 @@ class ElementSeeder extends Seeder . 'engender a shift in mindset, embed healthy routines, ' . 'positive life practices.', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], ]; @@ -97,7 +98,8 @@ class ElementSeeder extends Seeder description: $baderechChildElement['description'], iconImageUrl: null, richText: $baderechChildElement['richText'], - pdfPath: $baderechChildElement['pdfPath'], + shortPdfPath: $baderechChildElement['shortPdfPath'], + longPdfPath: null, youtubeUrl: null, parentElement: $rootElement, )); @@ -117,7 +119,8 @@ class ElementSeeder extends Seeder description: '', iconImageUrl: null, richText: '', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: 'https://www.youtube.com/watch?v=' . 'yHx-r4p6hHU&t=1s', parentElement: $introductionElement, diff --git a/backend/database/seeders/assets/element-pdfs/baderech.pdf b/backend/database/seeders/assets/element-pdfs/short/baderech.pdf similarity index 100% rename from backend/database/seeders/assets/element-pdfs/baderech.pdf rename to backend/database/seeders/assets/element-pdfs/short/baderech.pdf diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index 9c08e1c..18f1a6b 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -23,7 +23,8 @@ class FakeElementRepository implements ElementRepository description: $dto->description, iconImageUrl: $dto->iconImageUrl, richText: $dto->richText, - pdfPath: $dto->pdfPath, + shortPdfPath: $dto->shortPdfPath, + longPdfPath: $dto->longPdfPath, youtubeUrl: $dto->youtubeUrl, set: $dto->set, parentElement: $dto->parentElement, @@ -111,7 +112,8 @@ class FakeElementRepository implements ElementRepository description: $element->getDescription(), iconImageUrl: $element->getIconImageUrl(), richText: $element->getRichText(), - pdfPath: $element->getPdfPath(), + shortPdfPath: $element->getShortPdfPath(), + longPdfPath: $element->getLongPdfPath(), youtubeUrl: $element->getYoutubeUrl(), set: $element->getSet(), parentElement: $parentElement, diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 2c11199..345396c 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -341,7 +341,7 @@ class ElementsEndpointTest extends TestCase ]); } - public function testAuthenticatedIconImageUploadReturnsElementPayload(): void + public function testAuthenticatedIconImageUploadReturnsPayload(): void { Storage::fake('public'); $setRepository = app(SetRepository::class); diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts index 3155637..25d8b5c 100644 --- a/frontend/rabbi_gerzi/src/stores/elements.ts +++ b/frontend/rabbi_gerzi/src/stores/elements.ts @@ -1,4 +1,4 @@ -import { ref } from 'vue' +import { ref, type Ref } from 'vue' import { defineStore } from 'pinia' export interface ChildElement { @@ -10,10 +10,14 @@ export interface ChildElement { export interface Element extends ChildElement { iconImageUrl: string | null richText: string + shortPdfPath: string | null + longPdfPath: string | null pdfPath: string | null youtubeUrl: string | null } +type ElementFileType = 'iconImage' | 'shortPdf' | 'longPdf' + interface ElementResponse { element: Element childElements: ChildElement[] @@ -35,6 +39,8 @@ interface ElementPatchInput { description?: string iconImageUrl?: string richText?: string + shortPdfPath?: string + longPdfPath?: string pdfPath?: string youtubeUrl?: string } @@ -53,7 +59,8 @@ export const useElementsStore = defineStore('elements', () => { const error = ref(null) const isSaving = ref(false) const isUploadingIconImage = ref(false) - const isUploadingPdf = ref(false) + const isUploadingShortPdf = ref(false) + const isUploadingLongPdf = ref(false) const saveError = ref(null) async function fetchElement(elementId: string): Promise { @@ -89,16 +96,35 @@ export const useElementsStore = defineStore('elements', () => { } } - async function updateElement(elementId: string, input: UpdateElementInput): Promise { + async function updateElement( + elementId: string, + input: UpdateElementInput, + ): Promise { return await saveElementPatch(elementId, input, 'Could not save element') } async function clearElementIconImage(elementId: string): Promise { - return await saveElementPatch(elementId, { iconImageUrl: '' }, 'Could not remove icon image') + return await saveElementPatch( + elementId, + { iconImageUrl: '' }, + 'Could not remove icon image', + ) } - async function clearElementPdf(elementId: string): Promise { - return await saveElementPatch(elementId, { pdfPath: '' }, 'Could not remove PDF') + async function clearElementShortPdf(elementId: string): Promise { + return await saveElementPatch( + elementId, + { shortPdfPath: '' }, + 'Could not remove short PDF', + ) + } + + async function clearElementLongPdf(elementId: string): Promise { + return await saveElementPatch( + elementId, + { longPdfPath: '' }, + 'Could not remove long PDF', + ) } async function saveElementPatch( @@ -126,38 +152,64 @@ export const useElementsStore = defineStore('elements', () => { } } - async function uploadElementIconImage(elementId: string, file: File): Promise { - saveError.value = null - isUploadingIconImage.value = true - - try { - const formData = new FormData() - formData.append('elementId', elementId) - formData.append('iconImage', file) - const response = await fetch(ELEMENT_UPDATE_URL, { - method: 'POST', - headers: { Accept: 'application/json' }, - credentials: 'include', - body: formData, - }) - - return await handleElementResponse(response, 'Could not upload icon image') - } catch { - saveError.value = 'Network error - could not upload icon image' - return false - } finally { - isUploadingIconImage.value = false - } + async function uploadElementIconImage( + elementId: string, + file: File, + ): Promise { + return await uploadElementFile( + elementId, + file, + 'iconImage', + isUploadingIconImage, + 'Could not upload icon image', + 'Network error - could not upload icon image', + ) } - async function uploadElementPdf(elementId: string, file: File): Promise { + async function uploadElementShortPdf( + elementId: string, + file: File, + ): Promise { + return await uploadElementFile( + elementId, + file, + 'shortPdf', + isUploadingShortPdf, + 'Could not upload short PDF', + 'Network error - could not upload short PDF', + ) + } + + async function uploadElementLongPdf( + elementId: string, + file: File, + ): Promise { + return await uploadElementFile( + elementId, + file, + 'longPdf', + isUploadingLongPdf, + 'Could not upload long PDF', + 'Network error - could not upload long PDF', + ) + } + + async function uploadElementFile( + elementId: string, + file: File, + fileType: ElementFileType, + isUploadingFile: Ref, + failureMessage: string, + networkErrorMessage: string, + ): Promise { saveError.value = null - isUploadingPdf.value = true + isUploadingFile.value = true try { const formData = new FormData() formData.append('elementId', elementId) - formData.append('pdf', file) + formData.append('fileType', fileType) + formData.append('file', file) const response = await fetch(ELEMENT_UPDATE_URL, { method: 'POST', headers: { Accept: 'application/json' }, @@ -165,12 +217,12 @@ export const useElementsStore = defineStore('elements', () => { body: formData, }) - return await handleElementResponse(response, 'Could not upload PDF') + return await handleElementResponse(response, failureMessage) } catch { - saveError.value = 'Network error - could not upload PDF' + saveError.value = networkErrorMessage return false } finally { - isUploadingPdf.value = false + isUploadingFile.value = false } } @@ -203,7 +255,10 @@ export const useElementsStore = defineStore('elements', () => { return true } - async function errorMessage(response: Response, fallbackMessage: string): Promise { + async function errorMessage( + response: Response, + fallbackMessage: string, + ): Promise { try { const data: ErrorResponse = await response.json() return data.error ?? fallbackMessage @@ -219,13 +274,16 @@ export const useElementsStore = defineStore('elements', () => { error, isSaving, isUploadingIconImage, - isUploadingPdf, + isUploadingShortPdf, + isUploadingLongPdf, saveError, fetchElement, updateElement, uploadElementIconImage, - uploadElementPdf, + uploadElementShortPdf, + uploadElementLongPdf, clearElementIconImage, - clearElementPdf, + clearElementShortPdf, + clearElementLongPdf, } }) diff --git a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue index 37f03ff..261bac3 100644 --- a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue @@ -14,13 +14,23 @@ interface ElementForm { const route = useRoute() const elementsStore = useElementsStore() -const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } = - storeToRefs(elementsStore) +const { + element, + isLoading, + error, + isSaving, + isUploadingIconImage, + isUploadingShortPdf, + isUploadingLongPdf, + saveError, +} = storeToRefs(elementsStore) const savedMessage = ref(null) const iconImageStatus = ref(null) -const pdfStatus = ref(null) +const shortPdfStatus = ref(null) +const longPdfStatus = ref(null) const iconImageInput = ref(null) -const pdfInput = ref(null) +const shortPdfInput = ref(null) +const longPdfInput = ref(null) const form = reactive({ title: '', @@ -56,7 +66,8 @@ watch( savedMessage.value = null iconImageStatus.value = null - pdfStatus.value = null + shortPdfStatus.value = null + longPdfStatus.value = null void elementsStore.fetchElement(currentElementId) }, { immediate: true }, @@ -95,8 +106,12 @@ function chooseIconImage(): void { iconImageInput.value?.click() } -function choosePdf(): void { - pdfInput.value?.click() +function chooseShortPdf(): void { + shortPdfInput.value?.click() +} + +function chooseLongPdf(): void { + longPdfInput.value?.click() } async function handleIconImageChange(changeEvent: Event): Promise { @@ -107,7 +122,10 @@ async function handleIconImageChange(changeEvent: Event): Promise { savedMessage.value = null iconImageStatus.value = null - const uploaded = await elementsStore.uploadElementIconImage(elementId.value, selectedFile) + const uploaded = await elementsStore.uploadElementIconImage( + elementId.value, + selectedFile, + ) resetInput(changeEvent) if (uploaded) { @@ -115,19 +133,41 @@ async function handleIconImageChange(changeEvent: Event): Promise { } } -async function handlePdfChange(changeEvent: Event): Promise { +async function handleShortPdfChange(changeEvent: Event): Promise { const selectedFile = selectedInputFile(changeEvent) if (selectedFile === null || elementId.value === '') { return } savedMessage.value = null - pdfStatus.value = null - const uploaded = await elementsStore.uploadElementPdf(elementId.value, selectedFile) + shortPdfStatus.value = null + const uploaded = await elementsStore.uploadElementShortPdf( + elementId.value, + selectedFile, + ) resetInput(changeEvent) if (uploaded) { - pdfStatus.value = 'PDF updated' + shortPdfStatus.value = 'Short PDF updated' + } +} + +async function handleLongPdfChange(changeEvent: Event): Promise { + const selectedFile = selectedInputFile(changeEvent) + if (selectedFile === null || elementId.value === '') { + return + } + + savedMessage.value = null + longPdfStatus.value = null + const uploaded = await elementsStore.uploadElementLongPdf( + elementId.value, + selectedFile, + ) + resetInput(changeEvent) + + if (uploaded) { + longPdfStatus.value = 'Long PDF updated' } } @@ -144,16 +184,29 @@ async function handleRemoveIconImage(): Promise { } } -async function handleRemovePdf(): Promise { +async function handleRemoveShortPdf(): Promise { if (elementId.value === '') { return } savedMessage.value = null - pdfStatus.value = null - const removed = await elementsStore.clearElementPdf(elementId.value) + shortPdfStatus.value = null + const removed = await elementsStore.clearElementShortPdf(elementId.value) if (removed) { - pdfStatus.value = 'PDF removed' + shortPdfStatus.value = 'Short PDF removed' + } +} + +async function handleRemoveLongPdf(): Promise { + if (elementId.value === '') { + return + } + + savedMessage.value = null + longPdfStatus.value = null + const removed = await elementsStore.clearElementLongPdf(elementId.value) + if (removed) { + longPdfStatus.value = 'Long PDF removed' } } @@ -186,7 +239,9 @@ function resetInput(changeEvent: Event): void {

Edit Element

-

Loading element...

+

+ Loading element... +

{{ error }}

-
+