diff --git a/backend/Dockerfile b/backend/Dockerfile index b626f541cc..609a307da2 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,7 +9,7 @@ RUN echo "" >> /usr/local/etc/php-fpm.d/docker-php-serversideup-pool.conf && \ echo "user = www-data" >> /usr/local/etc/php-fpm.d/docker-php-serversideup-pool.conf && \ echo "group = www-data" >> /usr/local/etc/php-fpm.d/docker-php-serversideup-pool.conf -RUN install-php-extensions intl +RUN install-php-extensions intl imagick COPY --chown=www-data:www-data . . diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 9aed46137d..e4f9153f54 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -12,7 +12,7 @@ COPY --chown=www-data:www-data . /var/www/html # Switch to root user to install PHP extensions USER root -RUN install-php-extensions intl +RUN install-php-extensions intl imagick USER www-data RUN chmod -R 755 /var/www/html/storage \ diff --git a/backend/app/Console/Commands/BackfillImageMetadataCommand.php b/backend/app/Console/Commands/BackfillImageMetadataCommand.php new file mode 100644 index 0000000000..a15a0842a0 --- /dev/null +++ b/backend/app/Console/Commands/BackfillImageMetadataCommand.php @@ -0,0 +1,252 @@ +isImagickAvailable()) { + $this->error('Imagick extension is not available. Please install it first.'); + return self::FAILURE; + } + + $this->info('Starting image metadata backfill...'); + + $limit = (int)$this->option('limit'); + $batchSize = (int)$this->option('batch-size'); + $dryRun = $this->option('dry-run'); + $force = $this->option('force'); + + if ($dryRun) { + $this->warn('DRY RUN MODE - No changes will be made'); + } + + $query = Image::query() + ->whereNull('deleted_at'); + + if (!$force) { + $query->where(function ($q) { + $q->whereNull(ImageDomainObjectAbstract::WIDTH) + ->orWhereNull(ImageDomainObjectAbstract::HEIGHT) + ->orWhereNull(ImageDomainObjectAbstract::AVG_COLOUR) + ->orWhereNull(ImageDomainObjectAbstract::LQIP_BASE64); + }); + } + + $totalCount = $query->count(); + + if ($totalCount === 0) { + $this->info('No images found that need metadata backfill.'); + return self::SUCCESS; + } + + $toProcess = min($totalCount, $limit); + $this->info("Found {$totalCount} images without complete metadata. Processing {$toProcess}..."); + + $progressBar = $this->output->createProgressBar($toProcess); + $progressBar->start(); + + $successCount = 0; + $errorCount = 0; + $skippedCount = 0; + $processedInBatch = 0; + + $query->take($limit) + ->orderBy('id') + ->chunk($batchSize, function ($images) use ( + &$successCount, + &$errorCount, + &$skippedCount, + &$processedInBatch, + $progressBar, + $dryRun, + $batchSize, + ) { + foreach ($images as $image) { + try { + $result = $this->processImage($image, $dryRun); + + if ($result === 'success') { + $successCount++; + } elseif ($result === 'skipped') { + $skippedCount++; + } else { + $errorCount++; + } + } catch (Throwable $e) { + $this->newLine(); + $this->error("Failed to process image #{$image->id}: {$e->getMessage()}"); + $errorCount++; + } + + $progressBar->advance(); + $processedInBatch++; + + if ($processedInBatch >= $batchSize) { + gc_collect_cycles(); + $processedInBatch = 0; + } + } + }); + + $progressBar->finish(); + $this->newLine(2); + + $this->info('Backfill complete!'); + $this->table( + ['Status', 'Count'], + [ + ['Success', $successCount], + ['Errors', $errorCount], + ['Skipped', $skippedCount], + ['Total Processed', $successCount + $errorCount + $skippedCount], + ] + ); + + return $errorCount > 0 ? self::FAILURE : self::SUCCESS; + } + + private function processImage(Image $image, bool $dryRun): string + { + $disk = $image->disk; + $path = $image->path; + + if (!$disk || !$path) { + $this->logger->warning("Image #{$image->id} has no disk or path"); + return 'skipped'; + } + + $filesystem = $this->filesystemManager->disk($disk); + + if (!$filesystem->exists($path)) { + $this->logger->warning("Image file not found for image #{$image->id}: {$path}"); + return 'skipped'; + } + + if ($dryRun) { + $this->newLine(); + $this->line("Would process: Image #{$image->id}, Path: {$path}"); + return 'success'; + } + + $tempFile = null; + $imagick = null; + + try { + $tempFile = tempnam(sys_get_temp_dir(), 'img_backfill_'); + file_put_contents($tempFile, $filesystem->get($path)); + + $imagick = new Imagick($tempFile); + + $metadata = $this->extractMetadata($imagick); + + $image->update([ + ImageDomainObjectAbstract::WIDTH => $metadata->width, + ImageDomainObjectAbstract::HEIGHT => $metadata->height, + ImageDomainObjectAbstract::AVG_COLOUR => $metadata->avg_colour, + ImageDomainObjectAbstract::LQIP_BASE64 => $metadata->lqip_base64, + ]); + + return 'success'; + } finally { + if ($imagick !== null) { + $imagick->clear(); + $imagick->destroy(); + } + + if ($tempFile !== null && file_exists($tempFile)) { + unlink($tempFile); + } + } + } + + private function extractMetadata(Imagick $imagick): ImageMetadataDTO + { + $width = $imagick->getImageWidth(); + $height = $imagick->getImageHeight(); + $avgColour = $this->extractAverageColour($imagick); + $lqipBase64 = $this->generateLqip($imagick); + + return new ImageMetadataDTO( + width: $width, + height: $height, + avg_colour: $avgColour, + lqip_base64: $lqipBase64, + ); + } + + private function extractAverageColour(Imagick $imagick): string + { + $clone = clone $imagick; + $clone->resizeImage(1, 1, Imagick::FILTER_LANCZOS, 1); + $pixel = $clone->getImagePixelColor(0, 0); + $rgb = $pixel->getColor(); + $clone->clear(); + $clone->destroy(); + + return sprintf('#%02x%02x%02x', $rgb['r'], $rgb['g'], $rgb['b']); + } + + private function generateLqip(Imagick $imagick): string + { + $clone = clone $imagick; + + $width = $clone->getImageWidth(); + $height = $clone->getImageHeight(); + + if ($width > $height) { + $newWidth = self::LQIP_MAX_DIMENSION; + $newHeight = (int)round($height * (self::LQIP_MAX_DIMENSION / $width)); + } else { + $newHeight = self::LQIP_MAX_DIMENSION; + $newWidth = (int)round($width * (self::LQIP_MAX_DIMENSION / $height)); + } + + $newWidth = max(1, $newWidth); + $newHeight = max(1, $newHeight); + + $clone->resizeImage($newWidth, $newHeight, Imagick::FILTER_LANCZOS, 1); + $clone->setImageFormat('webp'); + $clone->setImageCompressionQuality(self::LQIP_QUALITY); + $clone->stripImage(); + + $blob = $clone->getImageBlob(); + $clone->clear(); + $clone->destroy(); + + return 'data:image/webp;base64,' . base64_encode($blob); + } + + private function isImagickAvailable(): bool + { + return extension_loaded('imagick') && class_exists(Imagick::class); + } +} diff --git a/backend/app/DomainObjects/Generated/ImageDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/ImageDomainObjectAbstract.php index 60059c09b4..f2d88a6680 100644 --- a/backend/app/DomainObjects/Generated/ImageDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/ImageDomainObjectAbstract.php @@ -23,6 +23,10 @@ abstract class ImageDomainObjectAbstract extends \HiEvents\DomainObjects\Abstrac final public const CREATED_AT = 'created_at'; final public const UPDATED_AT = 'updated_at'; final public const DELETED_AT = 'deleted_at'; + final public const WIDTH = 'width'; + final public const HEIGHT = 'height'; + final public const AVG_COLOUR = 'avg_colour'; + final public const LQIP_BASE64 = 'lqip_base64'; protected int $id; protected ?int $account_id = null; @@ -37,6 +41,10 @@ abstract class ImageDomainObjectAbstract extends \HiEvents\DomainObjects\Abstrac protected ?string $created_at = 'CURRENT_TIMESTAMP'; protected ?string $updated_at = 'CURRENT_TIMESTAMP'; protected ?string $deleted_at = null; + protected ?int $width = null; + protected ?int $height = null; + protected ?string $avg_colour = null; + protected ?string $lqip_base64 = null; public function toArray(): array { @@ -54,6 +62,10 @@ public function toArray(): array 'created_at' => $this->created_at ?? null, 'updated_at' => $this->updated_at ?? null, 'deleted_at' => $this->deleted_at ?? null, + 'width' => $this->width ?? null, + 'height' => $this->height ?? null, + 'avg_colour' => $this->avg_colour ?? null, + 'lqip_base64' => $this->lqip_base64 ?? null, ]; } @@ -199,4 +211,48 @@ public function getDeletedAt(): ?string { return $this->deleted_at; } + + public function setWidth(?int $width): self + { + $this->width = $width; + return $this; + } + + public function getWidth(): ?int + { + return $this->width; + } + + public function setHeight(?int $height): self + { + $this->height = $height; + return $this; + } + + public function getHeight(): ?int + { + return $this->height; + } + + public function setAvgColour(?string $avg_colour): self + { + $this->avg_colour = $avg_colour; + return $this; + } + + public function getAvgColour(): ?string + { + return $this->avg_colour; + } + + public function setLqipBase64(?string $lqip_base64): self + { + $this->lqip_base64 = $lqip_base64; + return $this; + } + + public function getLqipBase64(): ?string + { + return $this->lqip_base64; + } } diff --git a/backend/app/Resources/Image/ImageResource.php b/backend/app/Resources/Image/ImageResource.php index 26c4cbaf5d..244890e733 100644 --- a/backend/app/Resources/Image/ImageResource.php +++ b/backend/app/Resources/Image/ImageResource.php @@ -20,7 +20,11 @@ public function toArray($request): array 'size' => $this->getSize(), 'file_name' => $this->getFileName(), 'mime_type' => $this->getMimeType(), - 'type' => $this->getType() + 'type' => $this->getType(), + 'width' => $this->getWidth(), + 'height' => $this->getHeight(), + 'avg_colour' => $this->getAvgColour(), + 'lqip_base64' => $this->getLqipBase64(), ]; } } diff --git a/backend/app/Services/Domain/Image/ImageUploadService.php b/backend/app/Services/Domain/Image/ImageUploadService.php index 267080cf2d..c7fcf6f0f8 100644 --- a/backend/app/Services/Domain/Image/ImageUploadService.php +++ b/backend/app/Services/Domain/Image/ImageUploadService.php @@ -5,6 +5,7 @@ use HiEvents\DomainObjects\ImageDomainObject; use HiEvents\Repository\Interfaces\ImageRepositoryInterface; use HiEvents\Services\Infrastructure\Image\Exception\CouldNotUploadImageException; +use HiEvents\Services\Infrastructure\Image\ImageMetadataService; use HiEvents\Services\Infrastructure\Image\ImageStorageService; use Illuminate\Http\UploadedFile; @@ -12,9 +13,9 @@ class ImageUploadService { public function __construct( private readonly ImageStorageService $imageStorageService, - private readonly ImageRepositoryInterface $imageRepository - ) - { + private readonly ImageRepositoryInterface $imageRepository, + private readonly ImageMetadataService $imageMetadataService, + ) { } /** @@ -29,8 +30,9 @@ public function upload( ): ImageDomainObject { $storedImage = $this->imageStorageService->store($image, $imageType); + $metadata = $this->imageMetadataService->extractMetadata($image); - return $this->imageRepository->create([ + $data = [ 'account_id' => $accountId, 'entity_id' => $entityId, 'entity_type' => $entityType, @@ -40,6 +42,15 @@ public function upload( 'path' => $storedImage->path, 'size' => $storedImage->size, 'mime_type' => $storedImage->mime_type, - ]); + ]; + + if ($metadata !== null) { + $data['width'] = $metadata->width; + $data['height'] = $metadata->height; + $data['avg_colour'] = $metadata->avg_colour; + $data['lqip_base64'] = $metadata->lqip_base64; + } + + return $this->imageRepository->create($data); } } diff --git a/backend/app/Services/Infrastructure/Image/DTO/ImageMetadataDTO.php b/backend/app/Services/Infrastructure/Image/DTO/ImageMetadataDTO.php new file mode 100644 index 0000000000..17bc7982fb --- /dev/null +++ b/backend/app/Services/Infrastructure/Image/DTO/ImageMetadataDTO.php @@ -0,0 +1,14 @@ +isImagickAvailable()) { + return null; + } + + try { + $imagick = new Imagick($image->getRealPath()); + + $width = $imagick->getImageWidth(); + $height = $imagick->getImageHeight(); + $avgColour = $this->extractAverageColour($imagick); + $lqipBase64 = $this->generateLqip($imagick); + + $imagick->clear(); + $imagick->destroy(); + + return new ImageMetadataDTO( + width: $width, + height: $height, + avg_colour: $avgColour, + lqip_base64: $lqipBase64, + ); + } catch (\Exception $e) { + $this->logger->warning('Failed to extract image metadata: ' . $e->getMessage()); + + return null; + } + } + + private function isImagickAvailable(): bool + { + return extension_loaded('imagick') && class_exists(Imagick::class); + } + + private function extractAverageColour(Imagick $imagick): string + { + $clone = clone $imagick; + $clone->resizeImage(1, 1, Imagick::FILTER_LANCZOS, 1); + $pixel = $clone->getImagePixelColor(0, 0); + $rgb = $pixel->getColor(); + $clone->clear(); + $clone->destroy(); + + return sprintf('#%02x%02x%02x', $rgb['r'], $rgb['g'], $rgb['b']); + } + + private function generateLqip(Imagick $imagick): string + { + $clone = clone $imagick; + + $width = $clone->getImageWidth(); + $height = $clone->getImageHeight(); + + if ($width > $height) { + $newWidth = self::LQIP_MAX_DIMENSION; + $newHeight = (int) round($height * (self::LQIP_MAX_DIMENSION / $width)); + } else { + $newHeight = self::LQIP_MAX_DIMENSION; + $newWidth = (int) round($width * (self::LQIP_MAX_DIMENSION / $height)); + } + + $newWidth = max(1, $newWidth); + $newHeight = max(1, $newHeight); + + $clone->resizeImage($newWidth, $newHeight, Imagick::FILTER_LANCZOS, 1); + $clone->setImageFormat('webp'); + $clone->setImageCompressionQuality(self::LQIP_QUALITY); + $clone->stripImage(); + + $blob = $clone->getImageBlob(); + $clone->clear(); + $clone->destroy(); + + return 'data:image/webp;base64,' . base64_encode($blob); + } +} diff --git a/backend/composer.json b/backend/composer.json index bcb60aa744..55b0bb88f7 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -30,6 +30,9 @@ "stripe/stripe-php": "^17.0", "ext-xmlwriter": "*" }, + "suggest": { + "ext-imagick": "Required for image dimension extraction and LQIP generation" + }, "require-dev": { "druc/laravel-langscanner": "dev-l12-compatibility", "fakerphp/faker": "^1.9.1", diff --git a/backend/database/migrations/2025_12_07_150927_add_image_metadata_columns_to_images_table.php b/backend/database/migrations/2025_12_07_150927_add_image_metadata_columns_to_images_table.php new file mode 100644 index 0000000000..084d77fb6f --- /dev/null +++ b/backend/database/migrations/2025_12_07_150927_add_image_metadata_columns_to_images_table.php @@ -0,0 +1,31 @@ +unsignedInteger('width')->nullable()->after('mime_type'); + $table->unsignedInteger('height')->nullable()->after('width'); + $table->string('avg_colour', 7)->nullable()->after('height'); + $table->text('lqip_base64')->nullable()->after('avg_colour'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('images', function (Blueprint $table) { + $table->dropColumn(['width', 'height', 'avg_colour', 'lqip_base64']); + }); + } +}; diff --git a/backend/tests/Unit/Services/Domain/Image/ImageUploadServiceTest.php b/backend/tests/Unit/Services/Domain/Image/ImageUploadServiceTest.php index 4d684c3aac..9f8f4328b4 100644 --- a/backend/tests/Unit/Services/Domain/Image/ImageUploadServiceTest.php +++ b/backend/tests/Unit/Services/Domain/Image/ImageUploadServiceTest.php @@ -5,8 +5,10 @@ use HiEvents\DomainObjects\ImageDomainObject; use HiEvents\Repository\Interfaces\ImageRepositoryInterface; use HiEvents\Services\Domain\Image\ImageUploadService; +use HiEvents\Services\Infrastructure\Image\DTO\ImageMetadataDTO; use HiEvents\Services\Infrastructure\Image\DTO\ImageStorageResponseDTO; use HiEvents\Services\Infrastructure\Image\Exception\CouldNotUploadImageException; +use HiEvents\Services\Infrastructure\Image\ImageMetadataService; use HiEvents\Services\Infrastructure\Image\ImageStorageService; use Illuminate\Http\UploadedFile; use Mockery as m; @@ -16,6 +18,7 @@ class ImageUploadServiceTest extends TestCase { private ImageStorageService $imageStorageService; private ImageRepositoryInterface $imageRepository; + private ImageMetadataService $imageMetadataService; private ImageUploadService $service; protected function setUp(): void @@ -24,14 +27,72 @@ protected function setUp(): void $this->imageStorageService = m::mock(ImageStorageService::class); $this->imageRepository = m::mock(ImageRepositoryInterface::class); + $this->imageMetadataService = m::mock(ImageMetadataService::class); $this->service = new ImageUploadService( $this->imageStorageService, - $this->imageRepository + $this->imageRepository, + $this->imageMetadataService ); } - public function testUploadSuccessfullyCreatesImageRecord(): void + public function testUploadSuccessfullyCreatesImageRecordWithMetadata(): void + { + $uploadedFile = m::mock(UploadedFile::class); + $storedImage = new ImageStorageResponseDTO( + filename: 'foo.jpg', + disk: 'public', + path: 'images/foo.jpg', + size: 123456, + mime_type: 'image/jpeg' + ); + $metadata = new ImageMetadataDTO( + width: 800, + height: 600, + avg_colour: '#ff5500', + lqip_base64: 'data:image/webp;base64,abc123', + ); + $imageDomainObject = m::mock(ImageDomainObject::class); + $accountId = 123; + + $this->imageStorageService + ->shouldReceive('store') + ->once() + ->with($uploadedFile, 'profile') + ->andReturn($storedImage); + + $this->imageMetadataService + ->shouldReceive('extractMetadata') + ->once() + ->with($uploadedFile) + ->andReturn($metadata); + + $this->imageRepository + ->shouldReceive('create') + ->once() + ->with([ + 'account_id' => $accountId, + 'entity_id' => 1, + 'entity_type' => 'user', + 'type' => 'profile', + 'filename' => 'foo.jpg', + 'disk' => 'public', + 'path' => 'images/foo.jpg', + 'size' => 123456, + 'mime_type' => 'image/jpeg', + 'width' => 800, + 'height' => 600, + 'avg_colour' => '#ff5500', + 'lqip_base64' => 'data:image/webp;base64,abc123', + ]) + ->andReturn($imageDomainObject); + + $result = $this->service->upload($uploadedFile, 1, 'user', 'profile', $accountId); + + $this->assertSame($imageDomainObject, $result); + } + + public function testUploadSuccessfullyCreatesImageRecordWithoutMetadata(): void { $uploadedFile = m::mock(UploadedFile::class); $storedImage = new ImageStorageResponseDTO( @@ -50,6 +111,12 @@ public function testUploadSuccessfullyCreatesImageRecord(): void ->with($uploadedFile, 'profile') ->andReturn($storedImage); + $this->imageMetadataService + ->shouldReceive('extractMetadata') + ->once() + ->with($uploadedFile) + ->andReturn(null); + $this->imageRepository ->shouldReceive('create') ->once() diff --git a/backend/tests/Unit/Services/Infrastructure/Image/ImageMetadataServiceTest.php b/backend/tests/Unit/Services/Infrastructure/Image/ImageMetadataServiceTest.php new file mode 100644 index 0000000000..884e72a223 --- /dev/null +++ b/backend/tests/Unit/Services/Infrastructure/Image/ImageMetadataServiceTest.php @@ -0,0 +1,95 @@ +logger = m::mock(LoggerInterface::class); + $this->service = new ImageMetadataService($this->logger); + } + + public function testExtractMetadataReturnsNullWhenImagickNotAvailable(): void + { + if (extension_loaded('imagick')) { + $this->markTestSkipped('This test requires Imagick to NOT be installed'); + } + + $uploadedFile = m::mock(UploadedFile::class); + + $result = $this->service->extractMetadata($uploadedFile); + + $this->assertNull($result); + } + + public function testExtractMetadataReturnsMetadataWhenImagickAvailable(): void + { + if (!extension_loaded('imagick')) { + $this->markTestSkipped('This test requires Imagick to be installed'); + } + + $testImagePath = $this->createTestImage(); + $uploadedFile = new UploadedFile($testImagePath, 'test.png', 'image/png', null, true); + + $result = $this->service->extractMetadata($uploadedFile); + + $this->assertNotNull($result); + $this->assertEquals(100, $result->width); + $this->assertEquals(100, $result->height); + $this->assertMatchesRegularExpression('/^#[a-f0-9]{6}$/i', $result->avg_colour); + $this->assertStringStartsWith('data:image/webp;base64,', $result->lqip_base64); + + unlink($testImagePath); + } + + public function testExtractMetadataLogsWarningOnFailure(): void + { + if (!extension_loaded('imagick')) { + $this->markTestSkipped('This test requires Imagick to be installed'); + } + + $uploadedFile = m::mock(UploadedFile::class); + $uploadedFile->shouldReceive('getRealPath') + ->once() + ->andReturn('/nonexistent/path/to/image.png'); + + $this->logger->shouldReceive('warning') + ->once() + ->with(m::type('string')); + + $result = $this->service->extractMetadata($uploadedFile); + + $this->assertNull($result); + } + + private function createTestImage(): string + { + $imagick = new \Imagick(); + $imagick->newImage(100, 100, '#ff5500'); + $imagick->setImageFormat('png'); + + $tempPath = sys_get_temp_dir() . '/test_image_' . uniqid() . '.png'; + $imagick->writeImage($tempPath); + $imagick->destroy(); + + return $tempPath; + } + + protected function tearDown(): void + { + m::close(); + parent::tearDown(); + } +} diff --git a/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss b/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss index 5655771642..5fd6b18229 100644 --- a/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss +++ b/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss @@ -134,17 +134,47 @@ $transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1); width: 100%; max-height: 560px; overflow: hidden; + background-color: var(--accent-soft); + + // When aspect ratio is provided via CSS custom property, use it + // Otherwise fall back to auto sizing based on image + &[style*="--cover-aspect-ratio"] { + aspect-ratio: var(--cover-aspect-ratio); + } @include mixins.respond-below(md) { max-height: 400px; } } +.coverLqip { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + filter: blur(20px); + transform: scale(1.1); + z-index: 0; +} + .coverImage { width: 100%; - height: auto; display: block; transition: transform $transition-slow; + z-index: 1; + + // When wrapper has aspect ratio, image should fill it + .coverWrapper[style*="--cover-aspect-ratio"] & { + position: relative; + height: 100%; + object-fit: cover; + } + + // When no aspect ratio, let image determine height naturally + .coverWrapper:not([style*="--cover-aspect-ratio"]) & { + height: auto; + } .mainCard:hover & { transform: scale(1.02); diff --git a/frontend/src/components/layouts/EventHomepage/index.tsx b/frontend/src/components/layouts/EventHomepage/index.tsx index 56f3659b4b..8a29e63637 100644 --- a/frontend/src/components/layouts/EventHomepage/index.tsx +++ b/frontend/src/components/layouts/EventHomepage/index.tsx @@ -1,9 +1,9 @@ import classes from "./EventHomepage.module.scss"; import SelectProducts from "../../routes/product-widget/SelectProducts"; import "../../../styles/widget/default.scss"; -import {useEffect, useRef, useState} from "react"; +import React, {useEffect, useRef, useState} from "react"; import {EventDocumentHead} from "../../common/EventDocumentHead"; -import {eventCoverImageUrl, eventHomepageUrl, imageUrl, organizerHomepageUrl} from "../../../utilites/urlHelper.ts"; +import {eventCoverImage, eventHomepageUrl, imageUrl, organizerHomepageUrl} from "../../../utilites/urlHelper.ts"; import {Event, OrganizerStatus} from "../../../types.ts"; import {EventNotAvailable} from "./EventNotAvailable"; import { @@ -110,7 +110,8 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => { '--event-border-color': cssVars['--theme-border'], } as React.CSSProperties; - const coverImage = eventCoverImageUrl(event); + const coverImageData = eventCoverImage(event); + const coverImage = coverImageData?.url; const organizer = event.organizer!; const organizerSocials = organizer?.settings?.social_media_handles; const organizerLogo = imageUrl('ORGANIZER_LOGO', organizer?.images); @@ -209,7 +210,20 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => { {/* Hero Section */}