From aa4a9404b2146dd011a113d9a9a24d8687a114ae Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Fri, 9 Feb 2024 16:45:50 -0300 Subject: [PATCH] Feature: Add event tickets block, field template (#7217) --- .../Actions/EnqueueDonationFormScripts.php | 19 +- .../Actions/EnqueueFormBuilderScripts.php | 66 ++++++- .../Edit/BlockInspectorControls.tsx | 43 +++++ .../Edit/BlockPlaceholder.tsx | 34 ++++ .../Edit/BlockPlaceholderNoEvents.tsx | 23 +++ .../Edit/BlockPlaceholderSelectEvent.tsx | 27 +++ .../components/CreateEventNotice/index.tsx | 25 +++ .../components/CreateEventNotice/styles.scss | 31 +++ .../blocks/EventTicketsBlock/Edit/index.tsx | 32 ++++ .../blocks/EventTicketsBlock/Edit/styles.scss | 79 ++++++++ .../blocks/EventTicketsBlock/Icon.tsx | 25 +++ .../blocks/EventTicketsBlock/block.json | 15 ++ .../blocks/EventTicketsBlock/index.tsx | 19 ++ .../blocks/EventTicketsBlock/types.ts | 23 +++ src/EventTickets/resources/blocks/index.ts | 4 + .../components/EventTicketsDescription.tsx | 7 + .../components/EventTicketsHeader.tsx | 17 ++ .../resources/components/EventTicketsList.tsx | 29 +++ .../components/EventTicketsListItem.tsx | 38 ++++ .../resources/components/types.ts | 36 ++++ .../EventTickets/EventTicketsListHOC.tsx | 75 ++++++++ .../templates/EventTickets/index.tsx | 31 +++ .../templates/EventTickets/styles.scss | 178 ++++++++++++++++++ .../resources/templates/EventTickets/types.ts | 12 ++ src/EventTickets/resources/templates/index.ts | 3 + wordpress-scripts-webpack.config.js | 2 + 26 files changed, 891 insertions(+), 2 deletions(-) create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockInspectorControls.tsx create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholder.tsx create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholderNoEvents.tsx create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholderSelectEvent.tsx create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Edit/components/CreateEventNotice/index.tsx create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Edit/components/CreateEventNotice/styles.scss create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Edit/index.tsx create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Edit/styles.scss create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/Icon.tsx create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/block.json create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/index.tsx create mode 100644 src/EventTickets/resources/blocks/EventTicketsBlock/types.ts create mode 100644 src/EventTickets/resources/blocks/index.ts create mode 100644 src/EventTickets/resources/components/EventTicketsDescription.tsx create mode 100644 src/EventTickets/resources/components/EventTicketsHeader.tsx create mode 100644 src/EventTickets/resources/components/EventTicketsList.tsx create mode 100644 src/EventTickets/resources/components/EventTicketsListItem.tsx create mode 100644 src/EventTickets/resources/components/types.ts create mode 100644 src/EventTickets/resources/templates/EventTickets/EventTicketsListHOC.tsx create mode 100644 src/EventTickets/resources/templates/EventTickets/index.tsx create mode 100644 src/EventTickets/resources/templates/EventTickets/styles.scss create mode 100644 src/EventTickets/resources/templates/EventTickets/types.ts create mode 100644 src/EventTickets/resources/templates/index.ts diff --git a/src/EventTickets/Actions/EnqueueDonationFormScripts.php b/src/EventTickets/Actions/EnqueueDonationFormScripts.php index 60a69d18d3..db77ce2bec 100644 --- a/src/EventTickets/Actions/EnqueueDonationFormScripts.php +++ b/src/EventTickets/Actions/EnqueueDonationFormScripts.php @@ -2,6 +2,8 @@ namespace Give\EventTickets\Actions; +use Give\Framework\EnqueueScript; + /** * @unreleased */ @@ -9,6 +11,21 @@ class EnqueueDonationFormScripts { public function __invoke() { - // + $scriptAsset = require GIVE_PLUGIN_DIR . 'build/eventTicketsTemplate.asset.php'; + + (new EnqueueScript( + 'givewp-event-tickets-template', + 'build/eventTicketsTemplate.js', + GIVE_PLUGIN_DIR, + GIVE_PLUGIN_URL, + 'give' + ))->enqueue(); + + wp_enqueue_style( + 'givewp-event-tickets-template', + GIVE_PLUGIN_URL . 'build/eventTicketsTemplate.css', + [], + $scriptAsset['version'] + ); } } diff --git a/src/EventTickets/Actions/EnqueueFormBuilderScripts.php b/src/EventTickets/Actions/EnqueueFormBuilderScripts.php index 41645121a2..26f858b997 100644 --- a/src/EventTickets/Actions/EnqueueFormBuilderScripts.php +++ b/src/EventTickets/Actions/EnqueueFormBuilderScripts.php @@ -2,6 +2,8 @@ namespace Give\EventTickets\Actions; +use Give\Framework\EnqueueScript; + /** * @unreleased */ @@ -9,6 +11,68 @@ class EnqueueFormBuilderScripts { public function __invoke() { - // + $scriptAsset = require GIVE_PLUGIN_DIR . 'build/eventTicketsBlock.asset.php'; + + (new EnqueueScript( + 'givewp-event-tickets-block', + 'build/eventTicketsBlock.js', + GIVE_PLUGIN_DIR, + GIVE_PLUGIN_URL, + 'give' + ))->enqueue(); + + wp_localize_script( + 'givewp-event-tickets-block', + 'eventTicketsBlockSettings', + [ + 'events' => [ + [ + 'id' => 1, + 'title' => 'Event 1', + 'date' => '2024-01-10 10:00', + 'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + 'tickets' => [ + [ + 'id' => 1, + 'name' => 'Standard', + 'price' => 50, + 'quantity' => 5, + 'description' => 'Standard ticket description goes here', + ], + [ + 'id' => 2, + 'name' => 'VIP', + 'price' => 100, + 'quantity' => 5, + 'description' => 'VIP ticket description goes here', + ], + ], + ], + ], + // TODO: Update this to fetch events from the database + 'createEventUrl' => admin_url('edit.php?post_type=give_forms&page=give-event-tickets&action=new'), + //TODO: Update this with the correct URL + 'listEventsUrl' => admin_url('edit.php?post_type=give_forms&page=give-event-tickets'), + //TODO: Update this with the correct URL + 'ticketsLabel' => apply_filters( + 'givewp_event_tickets_block/tickets_label', + __('Select Tickets', 'give') + ), + 'soldOutMessage' => apply_filters( + 'givewp_event_tickets_block/sold_out_message', + __( + 'Thank you for supporting our cause. Our fundraising event tickets are officially sold out. You can still contribute by making a donation.', + 'give' + ) + ), + ] + ); + + wp_enqueue_style( + 'givewp-event-tickets-block', + GIVE_PLUGIN_URL . 'build/eventTicketsBlock.css', + [], + $scriptAsset['version'] + ); } } diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockInspectorControls.tsx b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockInspectorControls.tsx new file mode 100644 index 0000000000..f6d41778d1 --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockInspectorControls.tsx @@ -0,0 +1,43 @@ +import {InspectorControls} from '@wordpress/block-editor'; +import {PanelBody, PanelRow, SelectControl} from '@wordpress/components'; +import {createInterpolateElement} from '@wordpress/element'; +import {__} from '@wordpress/i18n'; +import CreateEventNotice from './components/CreateEventNotice'; + +/** + * @unreleased + */ +export default function BlockInspectorControls({attributes, setAttributes}) { + const {events} = window.eventTicketsBlockSettings; + const {eventId} = attributes; + + const eventOptions = + events.map((event) => { + return {label: event.title, value: `${event.id}`}; + }) ?? []; + + return ( + + + {events.length === 0 ? ( + + ) : ( + + events page.', 'give'), + { + a: , + } + )} + value={`${eventId}`} + options={[{label: 'Select', value: ''}, ...eventOptions]} + onChange={(value: string) => setAttributes({eventId: Number(value)})} + /> + + )} + + + ); +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholder.tsx b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholder.tsx new file mode 100644 index 0000000000..10f9da1564 --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholder.tsx @@ -0,0 +1,34 @@ +import EventTicketsHeader from '../../../components/EventTicketsHeader'; +import EventTicketsDescription from '../../../components/EventTicketsDescription'; +import EventTicketsList from '../../../components/EventTicketsList'; +import {getWindowData} from '@givewp/form-builder/common'; + +/** + * @unreleased + */ +export default function BlockPlaceholder({attributes}) { + const {events, ticketsLabel, soldOutMessage} = window.eventTicketsBlockSettings; + const event = events.find((event) => event.id === attributes.eventId); + const {currency} = getWindowData(); + + if (!event || !event.tickets.length) { + return null; + } + + return ( +
+
+ + + {event.description && } + + +
+
+ ); +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholderNoEvents.tsx b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholderNoEvents.tsx new file mode 100644 index 0000000000..e18c4773ea --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholderNoEvents.tsx @@ -0,0 +1,23 @@ +import {__} from '@wordpress/i18n'; +import {createInterpolateElement} from '@wordpress/element'; + +/** + * @unreleased + */ +export default function BlockPlaceholderNoEvents() { + return ( +
+

+ {createInterpolateElement( + __( + 'No events created yet. Go to the events page to create and manage your own event.', + 'give' + ), + { + a: , + } + )} +

+
+ ); +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholderSelectEvent.tsx b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholderSelectEvent.tsx new file mode 100644 index 0000000000..6cb040efea --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/BlockPlaceholderSelectEvent.tsx @@ -0,0 +1,27 @@ +import {__} from '@wordpress/i18n'; +import {SelectControl} from '@wordpress/components'; + +/** + * @unreleased + */ +export default function BlockPlaceholderSelectEvent({attributes, setAttributes}) { + const {events} = window.eventTicketsBlockSettings; + const eventOptions = + events.map((event) => { + return {label: event.title, value: `${event.id}`}; + }) ?? []; + + return ( +
+

+ {__('No event selected yet', 'give')} +

+ setAttributes({eventId: Number(value)})} + /> +
+ ); +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/components/CreateEventNotice/index.tsx b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/components/CreateEventNotice/index.tsx new file mode 100644 index 0000000000..21d902ee86 --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/components/CreateEventNotice/index.tsx @@ -0,0 +1,25 @@ +import {__} from '@wordpress/i18n'; +import {Notice} from '@wordpress/components'; +import {createInterpolateElement} from '@wordpress/element'; + +import './styles.scss'; + +/** + * @unreleased + */ +export default function CreateEventNotice() { + return ( +
+ +

{__('No event created yet', 'give')}

+

{__('Donors will not be able to see any event on this form', 'give')}

+

{createInterpolateElement( + __('Create an event', 'give'), + { + a: , + } + )}

+ +
+ ); +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/components/CreateEventNotice/styles.scss b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/components/CreateEventNotice/styles.scss new file mode 100644 index 0000000000..fbce162482 --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/components/CreateEventNotice/styles.scss @@ -0,0 +1,31 @@ +.givewp-event-tickets-block__create-event-notice { + margin-bottom: var(--givewp-spacing-4); + + .components-notice { + margin: 0; + padding: var(--givewp-spacing-3) var(--givewp-spacing-4); + + &__content { + margin: 0; + + h4, p { + font-size: 0.75rem; + line-height: 1.5; + margin: 0; + } + + p { + color: var(--givewp-grey-700); + margin-top: var(--givewp-spacing-2); + + a { + color: var(--givewp-grey-900); + + &:hover { + text-decoration: none; + } + } + } + } + } +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/index.tsx b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/index.tsx new file mode 100644 index 0000000000..f0a859b67d --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/index.tsx @@ -0,0 +1,32 @@ +import BlockInspectorControls from './BlockInspectorControls'; +import BlockPlaceholderNoEvents from './BlockPlaceholderNoEvents'; +import BlockPlaceholder from './BlockPlaceholder'; +import BlockPlaceholderSelectEvent from './BlockPlaceholderSelectEvent'; + +import './styles.scss'; + +/** + * @unreleased + */ +export default function Edit(props) { + const {events} = window.eventTicketsBlockSettings; + const { + attributes: {eventId}, + } = props; + + const eventIds = events.map((event) => event.id); + + return ( + <> + {events.length === 0 ? ( + + ) : !eventId || !eventIds.includes(eventId) ? ( + + ) : ( + + )} + + + + ); +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/styles.scss b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/styles.scss new file mode 100644 index 0000000000..72a9c899e1 --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Edit/styles.scss @@ -0,0 +1,79 @@ +@import "../../../templates/EventTickets/styles.scss"; + +.givewp-event-tickets-block__placeholder { + &--no-events, + &--select-event { + border: 1px dashed var(--givewp-grey-500); + border-radius: 0.3125rem; + padding: var(--givewp-spacing-6); + + p { + color: var(--givewp-grey-700); + font-size: 0.875rem; + margin: 0; + } + + a { + color: var(--givewp-grey-900); + font-weight: 600; + + &:hover { + text-decoration: none; + } + } + } + + &--select-event { + text-align: center; + + .components-base-control { + margin-top: var(--givewp-spacing-1); + + .components-select-control { + gap: var(--givewp-spacing-3); + + &__input { + border-color: var(--givewp-grey-400); + border-radius: 0.125rem; + color: var(--givewp-grey-500); + font-size: 0.8125rem !important; + height: 2.25rem !important; + padding: var(--givewp-spacing-2); + } + } + + .components-input-control { + &__label { + color: var(--givewp-grey-700); + font-size: 0.875rem !important; + font-weight: 400 !important; + text-align: center; + } + + &__container { + margin: 0 auto; + max-width: 15.75rem; + width: 100%; + } + } + } + } + + .givewp-event-tickets { + border: 1px solid var(--givewp-grey-500); + border-radius: 0.3125rem; + padding: var(--givewp-spacing-2); + + &__header { + padding: 0; + + &::before { + display: none; + } + + &__date { + color: var(--givewp-grey-900); + } + } + } +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/Icon.tsx b/src/EventTickets/resources/blocks/EventTicketsBlock/Icon.tsx new file mode 100644 index 0000000000..5f73301cc8 --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/Icon.tsx @@ -0,0 +1,25 @@ +import {Icon} from '@wordpress/icons'; +import {Path, SVG} from '@wordpress/components'; + +/** + * @unreleased + */ +export default function BlockIcon() { + return ( + + + + + } + /> + ); +} + diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/block.json b/src/EventTickets/resources/blocks/EventTicketsBlock/block.json new file mode 100644 index 0000000000..8a977a3eb6 --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/block.json @@ -0,0 +1,15 @@ +{ + "name": "givewp/event-tickets", + "title": "Event Tickets", + "description": "Sell event tickets for your cause", + "category": "input", + "icon": "yes", + "supports": { + "multiple": false + }, + "attributes": { + "eventId": { + "type": "number" + } + } +} diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/index.tsx b/src/EventTickets/resources/blocks/EventTicketsBlock/index.tsx new file mode 100644 index 0000000000..02ee26e7e3 --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/index.tsx @@ -0,0 +1,19 @@ +import metadata from './block.json'; +import Icon from './Icon'; +import Edit from './Edit'; + +/** + * @unreleased + */ +const settings = { + ...metadata, + icon: Icon, + edit: Edit, +}; + +const eventTicketsBlock = { + name: settings.name, + settings, +}; + +export default eventTicketsBlock; diff --git a/src/EventTickets/resources/blocks/EventTicketsBlock/types.ts b/src/EventTickets/resources/blocks/EventTicketsBlock/types.ts new file mode 100644 index 0000000000..e49ce92cac --- /dev/null +++ b/src/EventTickets/resources/blocks/EventTicketsBlock/types.ts @@ -0,0 +1,23 @@ +import {Ticket} from '../../components/types'; + +export type EventSettings = { + id: number; + title: string; + date: Date; + description: string; + tickets: Ticket[]; +}; + +export interface EventTicketsBlockSettings { + events: EventSettings[]; + createEventUrl: string; + listEventsUrl: string; + ticketsLabel: string; + soldOutMessage: string; +} + +declare global { + interface Window { + eventTicketsBlockSettings: EventTicketsBlockSettings; + } +} diff --git a/src/EventTickets/resources/blocks/index.ts b/src/EventTickets/resources/blocks/index.ts new file mode 100644 index 0000000000..9297ff7613 --- /dev/null +++ b/src/EventTickets/resources/blocks/index.ts @@ -0,0 +1,4 @@ +import {getBlockRegistrar} from '@givewp/form-builder/common/getWindowData'; +import eventTicketsBlock from './EventTicketsBlock'; + +getBlockRegistrar().register(eventTicketsBlock.name, eventTicketsBlock.settings); diff --git a/src/EventTickets/resources/components/EventTicketsDescription.tsx b/src/EventTickets/resources/components/EventTicketsDescription.tsx new file mode 100644 index 0000000000..352309806a --- /dev/null +++ b/src/EventTickets/resources/components/EventTicketsDescription.tsx @@ -0,0 +1,7 @@ +export default function EventTicketsDescription({description}) { + return ( +
+

{description}

+
+ ) +} diff --git a/src/EventTickets/resources/components/EventTicketsHeader.tsx b/src/EventTickets/resources/components/EventTicketsHeader.tsx new file mode 100644 index 0000000000..62f855aec4 --- /dev/null +++ b/src/EventTickets/resources/components/EventTicketsHeader.tsx @@ -0,0 +1,17 @@ +import moment from 'moment'; + +export default function EventTicketsHeader({title, date}) { + const fullDate = moment(date).format('dddd, MMMM Do, h:mma z'); + const day = moment(date).format('DD'); + const month = moment(date).format('MMM'); + + return ( +
+
+ {day} {month} +
+

{title}

+

{fullDate}

+
+ ); +} diff --git a/src/EventTickets/resources/components/EventTicketsList.tsx b/src/EventTickets/resources/components/EventTicketsList.tsx new file mode 100644 index 0000000000..6ba6bdc126 --- /dev/null +++ b/src/EventTickets/resources/components/EventTicketsList.tsx @@ -0,0 +1,29 @@ +import EventTicketsListItem from './EventTicketsListItem'; +import {EventTicketsListProps} from './types'; + +export default function EventTicketsList({ + tickets, + ticketsLabel, + soldOutMessage, + currency, + selectedTickets = [], + handleSelect = null, +}: EventTicketsListProps) { + return ( +
+

{ticketsLabel}

+ {tickets.map((ticket) => { + return ( + null + } + currency={currency} + /> + ); + })} +
+ ); +} diff --git a/src/EventTickets/resources/components/EventTicketsListItem.tsx b/src/EventTickets/resources/components/EventTicketsListItem.tsx new file mode 100644 index 0000000000..ae2ea09acd --- /dev/null +++ b/src/EventTickets/resources/components/EventTicketsListItem.tsx @@ -0,0 +1,38 @@ +import {Icon} from '@wordpress/components'; +import {__} from '@wordpress/i18n'; +import {plus, reset as minus} from '@wordpress/icons'; +import useCurrencyFormatter from '@givewp/forms/app/hooks/useCurrencyFormatter'; + +export default function EventTicketsListItem({ticket, currency, selectedTickets, handleSelect}) { + const formatter = useCurrencyFormatter(currency); + const ticketPrice = formatter.format(Number(ticket.price)); + + const handleButtonClick = (quantity) => (e) => { + e.preventDefault(); + handleSelect(quantity); + }; + + return ( +
+
+
{ticket.name}
+

{ticketPrice}

+

{ticket.description}

+
+
+
+ + + +
+

+ {ticket.quantity - selectedTickets} {__('remaining', 'give')} +

+
+
+ ); +} diff --git a/src/EventTickets/resources/components/types.ts b/src/EventTickets/resources/components/types.ts new file mode 100644 index 0000000000..eda4e3a183 --- /dev/null +++ b/src/EventTickets/resources/components/types.ts @@ -0,0 +1,36 @@ +import {OnSelectTicketProps} from '../templates/EventTickets/types'; + +export type Event = { + id: number; + name: string; + title: string; + date: Date; + description: string; + tickets: Ticket[]; + ticketsLabel: string; + soldOutMessage: string; +}; + +export type Ticket = { + id: number; + name: string; + price: number; + quantity: number; + description: string; +}; + +export type SelectedTicket = { + id: number; + quantity: number; + price: number; +}; + +export type EventTicketsListProps = { + tickets: Ticket[]; + ticketsLabel: string; + soldOutMessage: string; + currency: string; + selectedTickets?: SelectedTicket[]; + handleSelect?: OnSelectTicketProps; +}; + diff --git a/src/EventTickets/resources/templates/EventTickets/EventTicketsListHOC.tsx b/src/EventTickets/resources/templates/EventTickets/EventTicketsListHOC.tsx new file mode 100644 index 0000000000..4dedaaacbe --- /dev/null +++ b/src/EventTickets/resources/templates/EventTickets/EventTicketsListHOC.tsx @@ -0,0 +1,75 @@ +import {useEffect, useState} from 'react'; +import EventTicketsList from '../../components/EventTicketsList'; +import {EventTicketsListHOCProps, OnSelectTicketProps} from './types'; + +export default function EventTicketsListHOC({name, tickets, ticketsLabel, soldOutMessage}: EventTicketsListHOCProps) { + const [selectedTickets, setSelectedTickets] = useState([]); + const {useWatch, useCurrencyFormatter, useDonationSummary, useFormContext} = window.givewp.form.hooks; + const {setValue} = useFormContext(); + const currency = useWatch({name: 'currency'}); + const formatter = useCurrencyFormatter(currency); + const donationSummary = useDonationSummary(); + + useEffect(() => { + let amount = 0; + + Object.keys(selectedTickets).forEach((ticketId) => { + const ticket = tickets.find((ticket) => ticket.id === Number(ticketId)); + const quantity = selectedTickets[ticketId]?.quantity ?? 0; + + if (quantity > 0) { + donationSummary.addItem({ + id: `eventTickets-${ticketId}`, + label: `Ticket (${ticket.name})`, + value: formatter.format(ticket.price * quantity), + }); + amount += ticket.price * quantity; + } else { + donationSummary.removeItem(`eventTickets-${ticketId}`); + } + }); + + if (amount > 0) { + donationSummary.addToTotal('eventTickets', amount); + } else { + donationSummary.removeFromTotal('eventTickets'); + } + + setValue(name, JSON.stringify(Object.values(selectedTickets))); + }, [tickets, selectedTickets]); + + const onSelectTicket: OnSelectTicketProps = (ticketId, ticketQuantity, ticketPrice) => (selectedQuantity) => { + if (selectedQuantity < 0) { + selectedQuantity = 0; + } + + if (selectedQuantity > ticketQuantity) { + selectedQuantity = ticketQuantity; + } + + setSelectedTickets((selectedTickets) => { + if (selectedQuantity === 0) { + delete selectedTickets[ticketId]; + } else { + selectedTickets[ticketId] = { + ticketId, + quantity: selectedQuantity, + amount: selectedQuantity * ticketPrice, + }; + } + + return {...selectedTickets}; + }); + }; + + return ( + + ); +} diff --git a/src/EventTickets/resources/templates/EventTickets/index.tsx b/src/EventTickets/resources/templates/EventTickets/index.tsx new file mode 100644 index 0000000000..e80c68a9d4 --- /dev/null +++ b/src/EventTickets/resources/templates/EventTickets/index.tsx @@ -0,0 +1,31 @@ +import EventTicketsHeader from '../../components/EventTicketsHeader'; +import EventTicketsDescription from '../../components/EventTicketsDescription'; +import EventTicketsListHOC from './EventTicketsListHOC'; +import {Event} from '../../components/types'; + +import './styles.scss'; + +export default function EventTicketsField({ + id, + title, + date, + description, + tickets, + ticketsLabel, + soldOutMessage, +}: Event) { + return ( +
+ + + {description && } + + +
+ ); +} diff --git a/src/EventTickets/resources/templates/EventTickets/styles.scss b/src/EventTickets/resources/templates/EventTickets/styles.scss new file mode 100644 index 0000000000..3bc2305111 --- /dev/null +++ b/src/EventTickets/resources/templates/EventTickets/styles.scss @@ -0,0 +1,178 @@ +.givewp-event-tickets { + + &__header { + color: var(--givewp-grey-900); + column-gap: var(--givewp-spacing-2); + display: grid; + grid-template-columns: 3.5rem 1fr; + grid-template-rows: auto; + padding: var(--givewp-spacing-3) var(--givewp-spacing-4); + position: relative; + z-index: 1; + + &::before { + background-color: var(--givewp-primary-color); + content: ""; + height: 100%; + left: 0; + opacity: 0.1; + position: absolute; + top: 0; + width: 100%; + z-index: -1; + } + + &__date { + align-items: center; + background-color: var(--givewp-grey-25); + border-radius: var(--givewp-rounded-4); + color: var(--givewp-primary-color); + display: flex; + flex-direction: column-reverse; + font-size: 1.25rem; + font-weight: 600; + line-height: 1.4; + gap: var(--givewp-spacing-1); + grid-column: 1 / 2; + grid-row: 1 / 3; + height: 3.5rem; + justify-content: center; + + span { + color: var(--givewp-grey-900); + font-size: 0.625rem; + font-weight: 400; + text-transform: uppercase; + } + } + + &__title { + font-size: 1.125rem; + line-height: 1.56; + grid-column: 2 / 3; + grid-row: 1 / 2; + margin: 0; + } + + &__full-date { + color: var(--givewp-grey-700); + font-size: 0.75rem; + grid-column: 2 / 3; + grid-row: 2 / 3; + line-height: 1.5; + margin: 0; + } + } + + &__description { + margin-top: var(--givewp-spacing-5); + + p { + color: var(--givewp-grey-700); + font-size: 0.875rem; + line-height: 1.5; + margin: 0; + } + } + + &__tickets { + display: flex; + flex-direction: column; + gap: var(--givewp-spacing-2); + margin-top: var(--givewp-spacing-6); + + h4 { + color: var(--givewp-grey-900); + font-size: 1rem; + font-weight: 600; + line-height: 1.5; + margin: 0; + } + + &__ticket { + align-items: center; + border-radius: var(--givewp-rounded-2); + border: 1px solid var(--givewp-grey-25); + display: flex; + padding: var(--givewp-spacing-4); + + &__description { + display: flex; + flex: 1 0 auto; + flex-direction: column; + gap: var(--givewp-spacing-1); + + h5 { + color: var(--givewp-grey-900); + font-size: 1rem; + font-weight: 600; + line-height: 1.5; + margin: 0; + } + + p { + color: var(--givewp-grey-700); + font-size: 0.875rem; + line-height: 1.5; + margin: 0; + } + } + + &__quantity { + align-items: center; + display: flex; + flex-direction: column; + flex: 0 0 6.25rem; + gap: var(--givewp-spacing-2); + justify-content: center; + + &__input { + align-items: center; + display: flex; + gap: var(--givewp-spacing-2); + justify-content: center; + + button { + align-items: center; + background: none; + border: 0; + color: var(--givewp-grey-500); + display: flex; + height: 1.25rem; + line-height: 1.25rem; + padding: 0; + width: 1.25rem; + + &:hover { + cursor: pointer; + } + + svg { + height: 100%; + width: 100%; + } + } + + input { + border-radius: var(--givewp-rounded-2) !important; + border: 1px solid var(--givewp-grey-700); + color: var(--givewp-grey-700) !important; + flex: 0 0 2.75rem; + font-size: 1rem !important; + font-weight: 600 !important; + height: 2.25rem; + padding: var(--givewp-spacing-3) var(--givewp-spacing-2); + text-align: center; + } + } + + &__availability { + color: var(--givewp-grey-700); + font-size: 0.75rem; + line-height: 1.5; + margin: 0; + } + } + } + } +} diff --git a/src/EventTickets/resources/templates/EventTickets/types.ts b/src/EventTickets/resources/templates/EventTickets/types.ts new file mode 100644 index 0000000000..d7371342a8 --- /dev/null +++ b/src/EventTickets/resources/templates/EventTickets/types.ts @@ -0,0 +1,12 @@ +import {Ticket} from '../../components/types'; + +export type EventTicketsListHOCProps = { + name: string; + tickets: Ticket[]; + ticketsLabel: string; + soldOutMessage: string; +}; + +export interface OnSelectTicketProps { + (ticketId: number, ticketQuantity: number, ticketPrice: number): (selectedQuantity: number) => void; +} diff --git a/src/EventTickets/resources/templates/index.ts b/src/EventTickets/resources/templates/index.ts new file mode 100644 index 0000000000..441742fe0c --- /dev/null +++ b/src/EventTickets/resources/templates/index.ts @@ -0,0 +1,3 @@ +import EventTicketsField from './EventTickets'; + +window.givewp.form.templates.fields.eventTickets = EventTicketsField; diff --git a/wordpress-scripts-webpack.config.js b/wordpress-scripts-webpack.config.js index 37e155e4ef..9a23f59d41 100644 --- a/wordpress-scripts-webpack.config.js +++ b/wordpress-scripts-webpack.config.js @@ -34,6 +34,8 @@ module.exports = { donationFormRegistrars: srcPath('DonationForms/resources/registrars/index.ts'), donationFormEmbed: srcPath('DonationForms/resources/embed.ts'), donationFormEmbedInside: srcPath('DonationForms/resources/embedInside.ts'), + eventTicketsBlock: srcPath('EventTickets/resources/blocks/index.ts'), + eventTicketsTemplate: srcPath('EventTickets/resources/templates/index.ts'), stripePaymentElementGateway: srcPath( 'PaymentGateways/Gateways/Stripe/StripePaymentElementGateway/stripePaymentElementGateway.tsx' ),