Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ abstract class EventSettingDomainObjectAbstract extends \HiEvents\DomainObjects\
final public const ALLOW_COPY_DETAILS_TO_ALL_ATTENDEES = 'allow_copy_details_to_all_attendees';
final public const SHOW_AVAILABLE_OCCURRENCE_CAPACITY = 'show_available_occurrence_capacity';
final public const HIDE_SOLD_OUT_OCCURRENCES = 'hide_sold_out_occurrences';
final public const GET_TICKETS_BUTTON_TEXT = 'get_tickets_button_text';

protected int $id;
protected int $event_id;
Expand Down Expand Up @@ -129,6 +130,7 @@ abstract class EventSettingDomainObjectAbstract extends \HiEvents\DomainObjects\
protected bool $allow_copy_details_to_all_attendees = true;
protected bool $show_available_occurrence_capacity = false;
protected bool $hide_sold_out_occurrences = false;
protected ?string $get_tickets_button_text = null;

public function toArray(): array
{
Expand Down Expand Up @@ -192,6 +194,7 @@ public function toArray(): array
'allow_copy_details_to_all_attendees' => $this->allow_copy_details_to_all_attendees ?? null,
'show_available_occurrence_capacity' => $this->show_available_occurrence_capacity ?? null,
'hide_sold_out_occurrences' => $this->hide_sold_out_occurrences ?? null,
'get_tickets_button_text' => $this->get_tickets_button_text ?? null,
];
}

Expand Down Expand Up @@ -844,4 +847,15 @@ public function getHideSoldOutOccurrences(): bool
{
return $this->hide_sold_out_occurrences;
}

public function setGetTicketsButtonText(?string $get_tickets_button_text): self
{
$this->get_tickets_button_text = $get_tickets_button_text;
return $this;
}

public function getGetTicketsButtonText(): ?string
{
return $this->get_tickets_button_text;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function rules(): array
'email_footer_message' => ['string', 'nullable'],

'continue_button_text' => ['string', 'nullable', 'max:100'],
'get_tickets_button_text' => ['string', 'nullable', 'max:100'],
'support_email' => ['email', 'nullable'],
'require_attendee_details' => ['boolean'],
'attendee_details_collection_method' => [Rule::in(AttendeeDetailsCollectionMethod::valuesArray())],
Expand Down
1 change: 1 addition & 0 deletions backend/app/Resources/Event/EventSettingsResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function toArray($request): array
'post_checkout_message' => $this->getPostCheckoutMessage(),
'product_page_message' => $this->getProductPageMessage(),
'continue_button_text' => $this->getContinueButtonText(),
'get_tickets_button_text' => $this->getGetTicketsButtonText(),
'required_attendee_details' => $this->getRequireAttendeeDetails(),
'attendee_details_collection_method' => $this->getAttendeeDetailsCollectionMethod(),
'email_footer_message' => $this->getEmailFooterMessage(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function toArray($request): array

'product_page_message' => $this->getProductPageMessage(),
'continue_button_text' => $this->getContinueButtonText(),
'get_tickets_button_text' => $this->getGetTicketsButtonText(),
'required_attendee_details' => $this->getRequireAttendeeDetails(),
'attendee_details_collection_method' => $this->getAttendeeDetailsCollectionMethod(),
'email_footer_message' => $this->getEmailFooterMessage(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public function __construct(
// Waitlist settings
public readonly ?bool $waitlist_auto_process = null,
public readonly ?int $waitlist_offer_timeout_minutes = null,

public readonly ?string $get_tickets_button_text = null,
) {}

public static function createWithDefaults(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public function handle(PartialUpdateEventSettingsDTO $eventSettingsDTO): EventSe
'continue_button_text' => array_key_exists('continue_button_text', $eventSettingsDTO->settings)
? $eventSettingsDTO->settings['continue_button_text']
: $existingSettings->getContinueButtonText(),
'get_tickets_button_text' => array_key_exists('get_tickets_button_text', $eventSettingsDTO->settings)
? $eventSettingsDTO->settings['get_tickets_button_text']
: $existingSettings->getGetTicketsButtonText(),

'homepage_background_color' => $eventSettingsDTO->settings['homepage_background_color'] ?? $existingSettings->getHomepageBackgroundColor(),
'homepage_primary_color' => $eventSettingsDTO->settings['homepage_primary_color'] ?? $existingSettings->getHomepagePrimaryColor(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public function handle(UpdateEventSettingsDTO $settings): EventSettingDomainObje
'require_attendee_details' => $settings->require_attendee_details,
'attendee_details_collection_method' => $settings->attendee_details_collection_method->name,
'continue_button_text' => trim($settings->continue_button_text),
'get_tickets_button_text' => $settings->get_tickets_button_text === null
? null
: trim($settings->get_tickets_button_text),

'homepage_background_color' => $settings->homepage_background_color,
'homepage_primary_color' => $settings->homepage_primary_color,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('event_settings', static function (Blueprint $table) {
$table->string('get_tickets_button_text', 100)->nullable();
});
}

public function down(): void
{
Schema::table('event_settings', static function (Blueprint $table) {
$table->dropColumn('get_tickets_button_text');
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,40 @@ public function test_omitted_show_copy_details_key_falls_back_to_existing_value(
$this->assertFalse($dto->allow_copy_details_to_all_attendees);
}

public function test_explicit_get_tickets_button_text_is_passed_through(): void
{
$dto = $this->runPartialUpdate(
existingValue: true,
settings: ['get_tickets_button_text' => 'Grab a spot'],
existingGetTicketsButtonText: 'Get Tickets',
);

$this->assertSame('Grab a spot', $dto->get_tickets_button_text);
}

public function test_omitted_get_tickets_button_text_falls_back_to_existing_value(): void
{
$dto = $this->runPartialUpdate(
existingValue: true,
settings: [],
existingGetTicketsButtonText: 'Get Tickets',
);

$this->assertSame('Get Tickets', $dto->get_tickets_button_text);
}

/**
* Drives the partial handler and returns the UpdateEventSettingsDTO it forwards
* to the (mocked) full handler, so we can assert how the field was resolved.
*/
private function runPartialUpdate(bool $existingValue, array $settings): UpdateEventSettingsDTO
{
private function runPartialUpdate(
bool $existingValue,
array $settings,
?string $existingGetTicketsButtonText = null,
): UpdateEventSettingsDTO {
$existingSettings = (new EventSettingDomainObject)
->setAllowCopyDetailsToAllAttendees($existingValue)
->setGetTicketsButtonText($existingGetTicketsButtonText)
->setPaymentProviders([]);

$repository = Mockery::mock(EventSettingsRepositoryInterface::class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,72 @@ public function test_persists_allow_copy_details_to_all_attendees(): void
);
}

public function test_persists_trimmed_get_tickets_button_text(): void
{
Event::fake();

$existingSettings = new EventSettingDomainObject;

$this->eventSettingsRepository
->shouldReceive('findFirstWhere')
->with(['event_id' => 1])
->twice()
->andReturn($existingSettings);

$captured = ['missing'];
$this->eventSettingsRepository
->shouldReceive('updateWhere')
->once()
->andReturnUsing(function (...$args) use (&$captured) {
foreach ($args as $arg) {
if (is_array($arg) && array_key_exists('get_tickets_button_text', $arg)) {
$captured = [$arg['get_tickets_button_text']];
}
}

return 1;
});

$this->handler->handle($this->createDTO(get_tickets_button_text: ' Grab a spot '));

$this->assertSame(['Grab a spot'], $captured);
}

public function test_persists_null_get_tickets_button_text(): void
{
Event::fake();

$existingSettings = new EventSettingDomainObject;

$this->eventSettingsRepository
->shouldReceive('findFirstWhere')
->with(['event_id' => 1])
->twice()
->andReturn($existingSettings);

$captured = ['missing'];
$this->eventSettingsRepository
->shouldReceive('updateWhere')
->once()
->andReturnUsing(function (...$args) use (&$captured) {
foreach ($args as $arg) {
if (is_array($arg) && array_key_exists('get_tickets_button_text', $arg)) {
$captured = [$arg['get_tickets_button_text']];
}
}

return 1;
});

$this->handler->handle($this->createDTO());

$this->assertSame([null], $captured);
}

private function createDTO(
?bool $waitlist_auto_process = null,
bool $allow_copy_details_to_all_attendees = true,
?string $get_tickets_button_text = null,
): UpdateEventSettingsDTO {
return UpdateEventSettingsDTO::fromArray([
'account_id' => 1,
Expand All @@ -166,6 +229,7 @@ private function createDTO(
'pre_checkout_message' => null,
'email_footer_message' => null,
'continue_button_text' => 'Continue',
'get_tickets_button_text' => $get_tickets_button_text,
'support_email' => 'test@test.com',
'homepage_background_color' => '#ffffff',
'homepage_primary_color' => '#000000',
Expand Down
64 changes: 46 additions & 18 deletions frontend/src/components/layouts/EventHomepage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import classes from "./EventHomepage.module.scss";
import SelectProducts from "../../routes/product-widget/SelectProducts";
import "../../../styles/widget/default.scss";
import React, {useEffect, useRef, useState} from "react";
import React, {useCallback, useEffect, useRef, useState} from "react";
import {EventDocumentHead} from "../../common/EventDocumentHead";
import {eventCoverImage, eventHomepageUrl, imageUrl, organizerHomepageUrl} from "../../../utilites/urlHelper.ts";
import {Event, EventOccurrence, EventType, OrganizerStatus} from "../../../types.ts";
Expand Down Expand Up @@ -39,6 +39,7 @@ import {ShareComponent} from "../../common/ShareIcon";
import {EventDateRange} from "../../common/EventDateRange";
import {CalendarOptionsPopover} from "../../common/CalendarOptionsPopover";
import {isDateInPast} from "../../../utilites/dates.ts";
import {formatCurrency} from "../../../utilites/currency.ts";

interface EventHomepageProps {
event?: Event;
Expand All @@ -52,8 +53,31 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => {
const [showScrollButton, setShowScrollButton] = useState(false);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [selectedOccurrence, setSelectedOccurrence] = useState<EventOccurrence | undefined>();
const [selectedCart, setSelectedCart] = useState({quantity: 0, total: 0});
const [continueButtonNode, setContinueButtonNode] = useState<HTMLButtonElement | null>(null);
const [continueButtonInView, setContinueButtonInView] = useState(false);
const ticketsSectionRef = useRef<HTMLDivElement>(null);

const handleCartChange = useCallback(
(cart: {quantity: number; total: number}) => setSelectedCart(cart),
[],
);

useEffect(() => {
if (!continueButtonNode) {
setContinueButtonInView(false);
return;
}

const observer = new IntersectionObserver(
([entry]) => setContinueButtonInView(entry.isIntersecting),
{threshold: 0.5},
);
observer.observe(continueButtonNode);

return () => observer.disconnect();
}, [continueButtonNode]);

const {consentPending, consentGranted, onConsent} = useOrganizerTrackingPixels(
event?.organizer?.settings?.tracking_pixels
);
Expand Down Expand Up @@ -167,25 +191,17 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => {
const getStatusBadge = () => {
const products = event.products || event.product_categories?.flatMap(c => c.products || []) || [];

if (products.length === 0) {
return null;
}

const availableProducts = products.filter(p => p.is_available && !p.is_sold_out);
const allSoldOut = products.every(p => p.is_sold_out);

if (allSoldOut) {
return {text: t`Sold Out`, variant: 'danger'};
}

if (availableProducts.length === 0) {
return null;
if (products.length > 0 && products.every(p => p.is_sold_out)) {
return {text: t`Sold Out`};
}

return {text: t`Tickets Available`, variant: 'success'};
return null;
};

const statusBadge = getStatusBadge();
const getTicketsButtonText = event.settings?.get_tickets_button_text || t`Get Tickets`;
const continueButtonText = event.settings?.continue_button_text || t`Continue`;
const showFloatingCheckoutButton = selectedCart.quantity > 0 && !!continueButtonNode && !continueButtonInView;

return (
<>
Expand Down Expand Up @@ -561,6 +577,8 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => {
showPoweredBy={false}
initialOccurrenceId={initialOccurrenceId}
onSelectedOccurrenceChange={setSelectedOccurrence}
onCartChange={handleCartChange}
continueButtonRef={setContinueButtonNode}
/>
</div>

Expand Down Expand Up @@ -685,14 +703,24 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => {
</div>
</div>

{/* Floating Scroll Button */}
{showScrollButton && (
{showFloatingCheckoutButton && (
<button
className={classes.scrollToTicketsButton}
onClick={() => continueButtonNode?.click()}
>
<IconTicket size={18}/>
{selectedCart.total > 0
? `${continueButtonText} (${formatCurrency(selectedCart.total, event.currency)})`
: continueButtonText}
</button>
)}
{!showFloatingCheckoutButton && showScrollButton && (
<button
className={classes.scrollToTicketsButton}
onClick={scrollToTickets}
>
<IconTicket size={18}/>
{t`Get Tickets`}
{getTicketsButtonText}
</button>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import EventHomepage from "../EventHomepage";
interface PreviewSettings {
homepage_theme_settings?: Partial<HomepageThemeSettings>;
continue_button_text?: string;
get_tickets_button_text?: string;
}

const EventHomepagePreview = () => {
Expand Down Expand Up @@ -45,6 +46,7 @@ const EventHomepagePreview = () => {
...event.settings,
homepage_theme_settings: previewSettings.homepage_theme_settings as HomepageThemeSettings || event.settings.homepage_theme_settings,
continue_button_text: previewSettings.continue_button_text ?? event.settings.continue_button_text,
get_tickets_button_text: previewSettings.get_tickets_button_text ?? event.settings.get_tickets_button_text,
}
};
}
Expand Down
Loading
Loading