CatalogGiftView.tsx 31.1 Ko
Télécharger le fichier
import { Autocomplete } from '@base-ui/react/autocomplete';
import {
GetSessionDataManager,
GiftReceiverNotFoundEvent,
OctaneEvent,
NotEnoughBalanceMessageEvent,
PurchaseFromCatalogAsGiftComposer,
SecurityLevel
} from '@octane/renderer';
import { CSSProperties, FC, KeyboardEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ColorUtils,
GetConfigurationValue,
GiftWrappingConfiguration,
LocalizeText,
MessengerFriend,
OpenUrl,
ProductTypeEnum,
SendMessageComposer,
HasSessionPermission
} from '../../../../api';
import giftArrowLeftImage from '../../../../assets/images/catalog/air/gift/arrow-left.png';
import giftArrowRightImage from '../../../../assets/images/catalog/air/gift/arrow-right.png';
import giftCardBlankImage from '../../../../assets/images/catalog/air/gift/gift-card-blank.png';
import giftIncognitoImage from '../../../../assets/images/catalog/air/gift/incognito.png';
import giftPaletteBorderImage from '../../../../assets/images/catalog/air/gift/palette-border.png';
import giftPaletteSelectionImage from '../../../../assets/images/catalog/air/gift/palette-selection.png';
import giftSmallCoinImage from '../../../../assets/images/catalog/air/gift/small-coin.png';
import giftSmallPenImage from '../../../../assets/images/catalog/air/gift/small-pen.png';
import { LayoutAvatarImageView, LayoutFurniImageView, OctaneCardContentView, OctaneCardHeaderView, OctaneCardView } from '../../../../common';
import {
CatalogEvent,
CatalogInitGiftEvent,
CatalogPurchasedEvent,
CatalogPurchaseFailureEvent,
CatalogPurchaseNotAllowedEvent,
CatalogPurchaseSoldOutEvent
} from '../../../../events';
import { useCatalogActions, useCatalogUiState, useFriends, useGiftConfiguration, useMessageEvent, useNotification, useUiEvent } from '../../../../hooks';
import {
filterGiftRecipients,
findGiftRecipientMatchIndex,
getRandomGiftDefaultStuffType,
limitGiftMessageToTextarea,
resolveGiftWrappingSelection,
wrapGiftSelectionIndex
} from './CatalogGiftView.helpers';
import { Checkbox } from '../../../../common/Checkbox';
import { BaseButton } from '../../../../common/BaseButton';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
interface GiftPaletteColor {
id: number;
color: string;
}
const getValentineRibbonIndex = (configuration: GiftWrappingConfiguration): number => (configuration.ribbonTypes.length > 10 ? 10 : 0);
export const CatalogGiftView: FC = () => {
const [isVisible, setIsVisible] = useState(false);
const [initializationSequence, setInitializationSequence] = useState(0);
const [pageId, setPageId] = useState(0);
const [offerId, setOfferId] = useState(0);
const [extraData, setExtraData] = useState('');
const [receiverName, setReceiverName] = useState('');
const [message, setMessage] = useState('');
const [showMyFace, setShowMyFace] = useState(true);
const [isBuyingGift, setIsBuyingGift] = useState(false);
const [defaultStuffType, setDefaultStuffType] = useState<number | null>(null);
const [selectedBoxIndex, setSelectedBoxIndex] = useState(0);
const [selectedRibbonIndex, setSelectedRibbonIndex] = useState(0);
const [selectedColorId, setSelectedColorId] = useState(0);
const [suggestions, setSuggestions] = useState<MessengerFriend[]>([]);
const [isAutocompleteVisible, setIsAutocompleteVisible] = useState(false);
// The row the arrow keys / pointer highlight; Enter takes it, otherwise the first suggestion.
const highlightedSuggestionRef = useRef<string | undefined>(undefined);
const suggestionsHostRef = useRef<HTMLDivElement>(null);
const recipientInputRef = useRef<HTMLInputElement>(null);
const messageInputRef = useRef<HTMLTextAreaElement>(null);
const shouldFocusMessageRef = useRef(false);
const { friends } = useFriends();
const { data: giftConfiguration = null, refetch: refetchGiftConfiguration } = useGiftConfiguration();
const { resetPlacedOfferData = null } = useCatalogActions();
const { setGiftReceiver = null } = useCatalogUiState();
const { showConfirm = null, simpleAlert = null } = useNotification();
const sessionDataManager = GetSessionDataManager();
const isModerator = HasSessionPermission('acc_gift_hide_sender', SecurityLevel.MODERATOR);
const allFriends = useMemo(() => friends.filter((friend: MessengerFriend) => friend.id !== -1), [friends]);
const boxTypes = useMemo(() => {
if (!giftConfiguration || defaultStuffType === null) return [];
return [...giftConfiguration.boxTypes, defaultStuffType];
}, [defaultStuffType, giftConfiguration]);
const colors = useMemo<GiftPaletteColor[]>(() => {
if (!giftConfiguration) return [];
const result: GiftPaletteColor[] = [];
for (const colorId of giftConfiguration.stuffTypes) {
const giftData = sessionDataManager.getFloorItemData(colorId);
if (!giftData?.colors?.length) continue;
result.push({ id: colorId, color: ColorUtils.makeColorNumberHex(giftData.colors[0]) });
}
return result;
}, [giftConfiguration, sessionDataManager]);
const resetViewState = useCallback(() => {
setIsVisible(false);
setPageId(0);
setOfferId(0);
setExtraData('');
setReceiverName('');
setMessage('');
setShowMyFace(true);
setIsBuyingGift(false);
setDefaultStuffType(null);
setSelectedBoxIndex(0);
setSelectedRibbonIndex(0);
setSelectedColorId(0);
setSuggestions([]);
setIsAutocompleteVisible(false);
shouldFocusMessageRef.current = false;
}, []);
const onClose = useCallback(() => {
resetPlacedOfferData?.();
resetViewState();
}, [resetPlacedOfferData, resetViewState]);
useEffect(() => {
if (!isVisible || !giftConfiguration) return;
const nextDefaultStuffType = getRandomGiftDefaultStuffType(giftConfiguration.defaultStuffTypes);
const nextBoxTypes = [...giftConfiguration.boxTypes, nextDefaultStuffType];
const configuredBoxIndex = Number(GetConfigurationValue('catalog.purchase.gift_wrapping.default_box_index', 0));
const nextBoxIndex = configuredBoxIndex >= 0 && configuredBoxIndex < nextBoxTypes.length ? configuredBoxIndex : 0;
let nextRibbonIndex = giftConfiguration.ribbonTypes[0] ?? 0;
if (nextRibbonIndex < 0 || nextRibbonIndex >= giftConfiguration.ribbonTypes.length) nextRibbonIndex = 0;
if (nextBoxTypes[nextBoxIndex] === 8) nextRibbonIndex = getValentineRibbonIndex(giftConfiguration);
setDefaultStuffType(nextDefaultStuffType);
setSelectedBoxIndex(nextBoxIndex);
setSelectedRibbonIndex(nextRibbonIndex);
setSelectedColorId(giftConfiguration.stuffTypes[0] ?? 0);
}, [giftConfiguration, initializationSequence, isVisible]);
useEffect(() => {
if (!isVisible || defaultStuffType === null) return;
if (shouldFocusMessageRef.current) messageInputRef.current?.focus();
else recipientInputRef.current?.focus();
}, [defaultStuffType, initializationSequence, isVisible]);
const selectedBoxType = boxTypes[selectedBoxIndex] ?? 0;
const selectedRibbonType = giftConfiguration?.ribbonTypes[selectedRibbonIndex] ?? giftConfiguration?.ribbonTypes[0] ?? 0;
const isDefaultBox = defaultStuffType !== null && selectedBoxType === defaultStuffType;
const isValentineBox = selectedBoxType === 8;
const isRibbonEnabled = !isDefaultBox && !isValentineBox;
const isColorEnabled = isRibbonEnabled && !(selectedBoxType >= 3 && selectedBoxType <= 6);
const giftSelection = resolveGiftWrappingSelection(isDefaultBox, defaultStuffType ?? 0, selectedColorId, selectedBoxType, selectedRibbonType);
const showPurchaserIdentity = isModerator ? showMyFace : true;
const boxName = `catalog.gift_wrapping_new.box.${isDefaultBox ? 'default' : selectedBoxType}`;
const ribbonName = `catalog.gift_wrapping_new.ribbon.${selectedRibbonIndex}`;
const priceText = `catalog.gift_wrapping_new.${isDefaultBox ? 'freeprice' : 'price'}`;
const configuredGiftCard = GetConfigurationValue<string>('catalog.gift_wrapping_new.gift_card', '');
const giftCardImage = configuredGiftCard.length
? `${GetConfigurationValue<string>('image.library.url', '')}Giftcards/${configuredGiftCard}.png`
: giftCardBlankImage;
const selectRelativeBox = useCallback(
(offset: number) => {
if (!giftConfiguration || !boxTypes.length) return;
const nextIndex = wrapGiftSelectionIndex(selectedBoxIndex, offset, boxTypes.length);
setIsAutocompleteVisible(false);
setSelectedBoxIndex(nextIndex);
if (boxTypes[nextIndex] === 8) setSelectedRibbonIndex(getValentineRibbonIndex(giftConfiguration));
},
[boxTypes, giftConfiguration, selectedBoxIndex]
);
const selectRelativeRibbon = useCallback(
(offset: number) => {
if (!giftConfiguration || !isRibbonEnabled) return;
setIsAutocompleteVisible(false);
setSelectedRibbonIndex((value) => wrapGiftSelectionIndex(value, offset, giftConfiguration.ribbonTypes.length));
},
[giftConfiguration, isRibbonEnabled]
);
const selectReceiverName = useCallback((friendName: string) => {
setReceiverName(friendName);
setSuggestions([]);
setIsAutocompleteVisible(false);
messageInputRef.current?.focus();
}, []);
const onReceiverChanged = useCallback(
(value: string) => {
const nextSuggestions = filterGiftRecipients(allFriends, value);
setReceiverName(value);
setSuggestions(nextSuggestions);
setIsAutocompleteVisible(nextSuggestions.length > 0);
},
[allFriends]
);
const onReceiverKeyDown = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Tab') {
setIsAutocompleteVisible(false);
return;
}
if (event.key === 'ArrowDown' && !receiverName.length && (!suggestions.length || !isAutocompleteVisible)) {
const nextSuggestions = allFriends.slice(0, 10);
if (nextSuggestions.length) {
event.preventDefault();
setSuggestions(nextSuggestions);
setIsAutocompleteVisible(true);
}
return;
}
// Arrow keys and Enter on a highlighted row belong to the Base UI list; Enter with
// nothing highlighted still takes the first suggestion, like before.
if (event.key === 'Enter' && isAutocompleteVisible && suggestions.length && highlightedSuggestionRef.current === undefined) {
event.preventDefault();
selectReceiverName(suggestions[0].name);
}
},
[allFriends, isAutocompleteVisible, receiverName.length, selectReceiverName, suggestions]
);
const onBuyGift = useCallback(() => {
if (isBuyingGift) return;
setIsBuyingGift(true);
SendMessageComposer(
new PurchaseFromCatalogAsGiftComposer(
pageId,
offerId,
extraData,
receiverName,
message,
giftSelection.wrapperId,
giftSelection.boxType,
giftSelection.ribbonType,
showPurchaserIdentity
)
);
setGiftReceiver?.(null);
resetPlacedOfferData?.();
}, [extraData, giftSelection, isBuyingGift, message, offerId, pageId, receiverName, resetPlacedOfferData, setGiftReceiver, showPurchaserIdentity]);
const onGiftReceiverNotFound = useCallback(() => {
if (!isVisible || !isBuyingGift) return;
setIsBuyingGift(false);
simpleAlert?.(
LocalizeText('catalog.gift_wrapping.receiver_not_found.info'),
null,
null,
null,
LocalizeText('catalog.gift_wrapping.receiver_not_found.title')
);
}, [isBuyingGift, isVisible, simpleAlert]);
useMessageEvent<GiftReceiverNotFoundEvent>(GiftReceiverNotFoundEvent, onGiftReceiverNotFound);
const onNotEnoughBalance = useCallback(
(event: NotEnoughBalanceMessageEvent) => {
if (!isVisible || !isBuyingGift) return;
const parser = event.getParser();
setIsBuyingGift(false);
if (parser.notEnoughCredits) {
const title = LocalizeText('catalog.alert.notenough.title');
const description = LocalizeText('catalog.alert.notenough.credits.description');
if (showConfirm) {
const settle = () => resetPlacedOfferData?.();
showConfirm(
description,
() => {
settle();
const shopUrl = GetConfigurationValue<string>('web.shop.relativeUrl', '');
if (shopUrl) OpenUrl(shopUrl);
},
settle,
null,
null,
title
);
} else {
simpleAlert?.(description, null, null, null, title);
}
return;
}
if (!parser.notEnoughActivityPoints) return;
const currencyLocalization = GetConfigurationValue<string>(
`activitypoint.name.${parser.activityPointType}`,
parser.activityPointType === 0 ? 'tooltip.duckets' : ''
);
if (currencyLocalization) {
const currencyName = LocalizeText(currencyLocalization);
const description = LocalizeText('catalog.alert.notenough.activitypoints.description', ['currencyname'], [currencyName]);
const title = LocalizeText('catalog.alert.notenough.activitypoints.title', ['currencyname'], [currencyName]);
if (parser.activityPointType === 0 && showConfirm) {
const settle = () => resetPlacedOfferData?.();
showConfirm(
description,
() => {
settle();
const ducketsUrl = GetConfigurationValue<string>('link.format.duckets', '');
if (ducketsUrl) OpenUrl(ducketsUrl);
},
settle,
null,
null,
title
);
} else {
simpleAlert?.(description, null, null, null, title);
}
} else {
simpleAlert?.(
LocalizeText('catalog.alert.notenough.activitypoints.description'),
null,
null,
null,
LocalizeText(`catalog.alert.notenough.activitypoints.title.${parser.activityPointType}`)
);
}
},
[isBuyingGift, isVisible, resetPlacedOfferData, showConfirm, simpleAlert]
);
useMessageEvent<NotEnoughBalanceMessageEvent>(NotEnoughBalanceMessageEvent, onNotEnoughBalance);
const onCatalogEvent = useCallback(
(event: OctaneEvent) => {
if (event.type === CatalogEvent.INIT_GIFT) {
const giftEvent = event as CatalogInitGiftEvent;
const initialReceiverName = giftEvent.receiverName ?? '';
// Opening the customizer is a continuation of the purchase flow.
// AIR only clears a placed-offer preview on submit or cancellation.
resetViewState();
setPageId(giftEvent.pageId);
setOfferId(giftEvent.offerId);
setExtraData(giftEvent.extraData);
setReceiverName(initialReceiverName);
shouldFocusMessageRef.current = initialReceiverName.length > 0;
setInitializationSequence((value) => value + 1);
setIsVisible(true);
// The server also pushes this during login, but AIR requests
// the wrapping configuration explicitly. Retry here when the
// cached request was missed or failed so gifting cannot end in
// an invisible second stage.
if (!giftConfiguration) void refetchGiftConfiguration();
return;
}
// Purchase outcome events are shared by the whole catalog. Only
// consume one while this dialog owns an in-flight gift purchase.
if (!isVisible || !isBuyingGift) return;
switch (event.type) {
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
onClose();
return;
case CatalogPurchaseFailureEvent.PURCHASE_FAILED: {
const purchaseError = event as CatalogPurchaseFailureEvent;
const descriptionKey =
purchaseError.code > 0 ? `catalog.alert.purchaseerror.description.${purchaseError.code}` : 'catalog.alert.purchaseerror.description';
simpleAlert?.(LocalizeText(descriptionKey), null, null, null, LocalizeText('catalog.alert.purchaseerror.title'));
onClose();
return;
}
case CatalogPurchaseNotAllowedEvent.NOT_ALLOWED: {
const notAllowed = event as CatalogPurchaseNotAllowedEvent;
const descriptionKey =
notAllowed.code === 1 ? 'catalog.alert.purchasenotallowed.hc.description' : 'catalog.alert.purchasenotallowed.unknown.description';
// Polaris uses this response for insufficient gift funds,
// so unlock the AIR submit control just as its dedicated
// NotEnoughBalance response does.
setIsBuyingGift(false);
simpleAlert?.(LocalizeText(descriptionKey), null, null, null, LocalizeText('catalog.alert.purchasenotallowed.title'));
return;
}
case CatalogPurchaseSoldOutEvent.SOLD_OUT:
simpleAlert?.(
LocalizeText('catalog.alert.limited_edition_sold_out.message'),
null,
null,
null,
LocalizeText('catalog.alert.limited_edition_sold_out.title')
);
onClose();
return;
}
},
[giftConfiguration, isBuyingGift, isVisible, onClose, refetchGiftConfiguration, resetViewState, simpleAlert]
);
useUiEvent(
[
CatalogPurchasedEvent.PURCHASE_SUCCESS,
CatalogPurchaseFailureEvent.PURCHASE_FAILED,
CatalogPurchaseNotAllowedEvent.NOT_ALLOWED,
CatalogPurchaseSoldOutEvent.SOLD_OUT,
CatalogEvent.INIT_GIFT
],
onCatalogEvent
);
const renderSuggestionName = useCallback(
(name: string): ReactNode => {
const matchIndex = findGiftRecipientMatchIndex(name, receiverName);
if (matchIndex < 0 || !receiverName.length) return name;
const matchEnd = Math.min(matchIndex + receiverName.length, name.length);
return (
<NativeTextTree>
<>
{name.slice(0, matchIndex)}
<strong>{name.slice(matchIndex, matchEnd)}</strong>
{name.slice(matchEnd)}
</>
</NativeTextTree>
);
},
[receiverName]
);
// AIR caches the wrapping configuration when the catalog starts, but
// showGiftDialog() never gates the customizer on isWrappingEnabled. Once
// usable wrapper data exists, the second stage of the gift flow opens.
if (!isVisible || !giftConfiguration || !boxTypes.length) return null;
return (
<NativeTextTree>
<OctaneCardView classNames={['octane-catalog-gift']} frameStyle={3} isResizable={false} theme="primary-slim">
<OctaneCardHeaderView headerText={LocalizeText('catalog.gift_wrapping.title')} onCloseClick={onClose} />
<OctaneCardContentView classNames={['octane-catalog-gift-content']} overflow="hidden">
<div ref={suggestionsHostRef} className="octane-catalog-gift-suggestions-host" />
<Autocomplete.Root
filter={null}
items={suggestions.map((friend) => friend.name)}
mode="list"
open={isAutocompleteVisible && suggestions.length > 0}
value={receiverName}
onItemHighlighted={(item) => {
highlightedSuggestionRef.current = item;
}}
onOpenChange={(open) => {
if (!open) setIsAutocompleteVisible(false);
}}
onValueChange={(next, details) => {
if (details.reason === 'item-press') selectReceiverName(next);
else onReceiverChanged(next);
}}
>
<div className="octane-catalog-gift-name-border">
<Autocomplete.Input
aria-label={LocalizeText('catalog.gift_wrapping_new.name_hint')}
className="octane-catalog-gift-name-input"
maxLength={32}
placeholder={LocalizeText('catalog.gift_wrapping_new.name_hint')}
ref={recipientInputRef}
onKeyDown={onReceiverKeyDown}
onMouseDown={() => setIsAutocompleteVisible(false)}
/>
</div>
<img alt="" className="octane-catalog-gift-pen" draggable={false} src={giftSmallPenImage} />
<Autocomplete.Portal container={suggestionsHostRef}>
<Autocomplete.Positioner className="octane-catalog-gift-suggestions-positioner">
<Autocomplete.Popup>
<Autocomplete.List className="octane-catalog-gift-suggestions">
{(name: string) => (
<Autocomplete.Item key={name} value={name}>
{renderSuggestionName(name)}
</Autocomplete.Item>
)}
</Autocomplete.List>
</Autocomplete.Popup>
</Autocomplete.Positioner>
</Autocomplete.Portal>
</Autocomplete.Root>
<img alt="" className="octane-catalog-gift-card" draggable={false} src={giftCardImage} />
<div className="octane-catalog-gift-avatar">
{showPurchaserIdentity ? (
<LayoutAvatarImageView
headOnly
classNames={['octane-catalog-gift-avatar-image']}
direction={2}
figure={sessionDataManager.figure}
nativeCroppedHead
/>
) : (
<img alt="" className="octane-catalog-gift-incognito" draggable={false} src={giftIncognitoImage} />
)}
</div>
<textarea
aria-label={LocalizeText('catalog.gift_wrapping_new.message_hint')}
className={`octane-catalog-gift-message${isAutocompleteVisible && suggestions.length >= 2 ? ' is-concealed' : ''}`}
maxLength={140}
placeholder={LocalizeText('catalog.gift_wrapping_new.message_hint')}
ref={messageInputRef}
value={message}
onChange={(event) => setMessage(limitGiftMessageToTextarea(event.target.value, event.target))}
onFocus={() => setIsAutocompleteVisible(false)}
/>
{showPurchaserIdentity && (
<div className="octane-catalog-gift-signature">
{LocalizeText('catalog.gift_wrapping_new.message_from', ['name'], [sessionDataManager.userName])}
</div>
)}
{isModerator && (
<>
<Checkbox aria-label={LocalizeText('catalog.gift_wrapping.show_face.title')} checked={showMyFace} className="octane-catalog-gift-show-face" id="octane-catalog-gift-show-face" onCheckedChange={(nextChecked) => setShowMyFace(nextChecked)} />
<label className="octane-catalog-gift-show-face-label" htmlFor="octane-catalog-gift-show-face">
{LocalizeText('catalog.gift_wrapping.show_face.title')}
</label>
</>
)}
<div className="octane-catalog-gift-box-picker">
<div className="octane-catalog-gift-product-border">
<div className="octane-catalog-gift-product-image">
{giftSelection.wrapperId > 0 && (
<LayoutFurniImageView
direction={180}
extraData={giftSelection.previewExtraData}
productClassId={giftSelection.wrapperId}
productType={ProductTypeEnum.FLOOR}
/>
)}
</div>
</div>
<BaseButton
aria-label={LocalizeText('catalog.gift_wrapping.pick_box')}
className="octane-catalog-gift-arrow octane-catalog-gift-box-prev"
type="button"
onClick={() => selectRelativeBox(-1)}
>
<img alt="" draggable={false} src={giftArrowLeftImage} />
</BaseButton>
<BaseButton
aria-label={LocalizeText('catalog.gift_wrapping.pick_box')}
className="octane-catalog-gift-arrow octane-catalog-gift-box-next"
type="button"
onClick={() => selectRelativeBox(1)}
>
<img alt="" draggable={false} src={giftArrowRightImage} />
</BaseButton>
<div className="octane-catalog-gift-box-name">{LocalizeText(boxName)}</div>
<div className="octane-catalog-gift-price">
<span>{LocalizeText(priceText, ['price'], [giftConfiguration.price.toString()])}</span>
{!isDefaultBox && <img alt="" draggable={false} src={giftSmallCoinImage} />}
</div>
<BaseButton
aria-label={LocalizeText('catalog.gift_wrapping.pick_ribbon')}
className={`octane-catalog-gift-arrow octane-catalog-gift-ribbon-prev${isRibbonEnabled ? '' : ' is-disabled'}`}
disabled={!isRibbonEnabled}
type="button"
onClick={() => selectRelativeRibbon(-1)}
>
<img alt="" draggable={false} src={giftArrowLeftImage} />
</BaseButton>
<BaseButton
aria-label={LocalizeText('catalog.gift_wrapping.pick_ribbon')}
className={`octane-catalog-gift-arrow octane-catalog-gift-ribbon-next${isRibbonEnabled ? '' : ' is-disabled'}`}
disabled={!isRibbonEnabled}
type="button"
onClick={() => selectRelativeRibbon(1)}
>
<img alt="" draggable={false} src={giftArrowRightImage} />
</BaseButton>
<div className={`octane-catalog-gift-ribbon-name${isRibbonEnabled ? '' : ' is-disabled'}`}>{LocalizeText(ribbonName)}</div>
</div>
<div className={`octane-catalog-gift-color-title${isColorEnabled ? '' : ' is-disabled'}`}>
{LocalizeText('catalog.gift_wrapping.pick_color')}
</div>
<div className={`octane-catalog-gift-palette${isColorEnabled ? '' : ' is-disabled'}`}>
<div className="octane-catalog-gift-palette-grid">
{colors.map((color) => (
<BaseButton
aria-label={color.color}
className="octane-catalog-gift-palette-item"
disabled={!isColorEnabled}
key={color.id}
style={{ '--octane-gift-palette-color': color.color } as CSSProperties}
type="button"
onClick={() => {
setIsAutocompleteVisible(false);
setSelectedColorId(color.id);
}}
>
<span className="octane-catalog-gift-palette-color" />
<img alt="" className="octane-catalog-gift-palette-border" draggable={false} src={giftPaletteBorderImage} />
{color.id === selectedColorId && (
<img alt="" className="octane-catalog-gift-palette-selection" draggable={false} src={giftPaletteSelectionImage} />
)}
</BaseButton>
))}
</div>
</div>
<BaseButton className="octane-catalog-gift-cancel" type="button" onClick={onClose}>
{LocalizeText('catalog.gift_wrapping.cancel')}
</BaseButton>
<BaseButton className="octane-catalog-gift-submit" disabled={isBuyingGift} type="button" onClick={onBuyGift}>
{LocalizeText('catalog.gift_wrapping.give_gift')}
</BaseButton>
</OctaneCardContentView>
</OctaneCardView>
</NativeTextTree>
);
};
